# -*- coding: utf-8 -*-
import datetime as dt_module
import re
import sys
from collections import namedtuple
from datetime import date, datetime, timedelta
from numpy import (
    array, concatenate, cos, float64, int64, isnan, isinf, linspace,
    nan, ndarray, nonzero, pi, rollaxis, searchsorted, sin, where, zeros_like,

)
from time import strftime, struct_time
from ._compatibility import interp
from .constants import ASEC2RAD, B1950, DAY_S, T0, tau
from .curvelib import Splines, build_spline_given_ends
from .descriptorlib import reify
from .earthlib import sidereal_time, earth_rotation_angle
from .framelib import ICRS_to_J2000 as B
from .functions import (A, mxm, mxmxm, load_bundled_npy, rot_x, rot_y, rot_z,
                        _to_array, _reconcile)
from .nutationlib import (
    build_nutation_matrix, equation_of_the_equinoxes_complimentary_terms,
    iau2000a_radians, mean_obliquity,
)
from .precessionlib import compute_precession

_EMPTY_TUPLE = ()  # since we use the value as a sentinel
DAY_US = 86400000000.0
GREGORIAN_START = 2299161
GREGORIAN_START_ENGLAND = 2361222
_OLD_PYTHON = sys.version_info < (2, 7)
def _cat(*args): return concatenate(args, axis=1)

CalendarTuple = namedtuple('CalendarTuple', 'year month day hour minute second')

class CalendarArray(ndarray):
    @property
    def year(self): return self[0]
    @property
    def month(self): return self[1]
    @property
    def day(self): return self[2]
    @property
    def hour(self): return self[3]
    @property
    def minute(self): return self[4]
    @property
    def second(self): return self[5]

if hasattr(dt_module, 'timezone'):
    utc = dt_module.timezone.utc
    @staticmethod
    def _utcnow():
        return datetime.now(utc)  # datetime.UTC was not added until 3.11
else:
    class UTC(dt_module.tzinfo):
        'UTC'
        zero = timedelta(0)
        def utcoffset(self, dt):
            return self.zero
        def tzname(self, dt):
            return 'UTC'
        def dst(self, dt):
            return self.zero

    utc = UTC()
    _utcnow = datetime.utcnow

# Much of the following code is adapted from the USNO's "novas.c".

_time_zero = dt_module.time(tzinfo=utc)
MONTH_NAMES = A['0', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

tt_minus_tai = array(32.184 / DAY_S)

class Timescale(object):
    """The data necessary to express dates in different timescales.

    A `Timescale` provides time objects with the data tables they need
    to translate between different time scales: the schedule of UTC leap
    seconds, and the value of ∆T over time.  Most programs create a
    single `Timescale` which they use to build their `Time` objects:

    >>> from skyfield.api import load
    >>> ts = load.timescale()
    >>> t = ts.utc(1980, 3, 1, 9, 30)
    >>> t
    <Time tt=2444299.896425741>

    See :ref:`downloading-timescale-files` if you are interested in
    checking how recent the data is in the files loaded by the
    timescale.

    """
    _utcnow = _utcnow
    polar_motion_table = None

    def __init__(self, delta_t_recent, leap_dates, leap_offsets):
        if callable(delta_t_recent):
            # Let a caller completely override our approach to ∆T by
            # passing a function of their own.
            self.delta_t_function = delta_t_recent
        else:
            self.delta_t_table = delta_t_recent  # so users can see it
            self.delta_t_function = build_delta_t(delta_t_recent)

        self.leap_dates, self.leap_offsets = leap_dates, leap_offsets
        self.J2000 = Time(self, float64(T0))
        self.B1950 = Time(self, float64(B1950))
        self.julian_calendar_cutoff = None

        # Our internal leap-second table has three columns:
        #
        # _leap_utc      Integer UTC seconds since JD 0 w/leap seconds missing.
        # _leap_offsets  Integer offset between UTC and TAI.
        # _leap_tai      Integer TAI seconds since JD 0.

        is_legacy_table = isinf(leap_dates[-1])
        if is_legacy_table:
            leap_dates = leap_dates[2:-1]
            leap_offsets = leap_offsets[3:]

        one_zero = [[-1,0]]
        self._leap_utc = (leap_dates[:,None] * DAY_S + one_zero).flatten()
        self._leap_offsets = (leap_offsets[:,None] + one_zero).flatten()
        self._leap_tai = self._leap_utc + self._leap_offsets

    def now(self):
        """Return the current date and time as a `Time` object."""
        return self.from_datetime(self._utcnow().replace(tzinfo=utc))

    def from_datetime(self, datetime):
        """Return a `Time` for a Python ``datetime``.

        The ``datetime`` must be “timezone-aware”: it must have a time
        zone object as its ``tzinfo`` attribute instead of ``None``.

        .. versionadded:: 1.24

        """
        return self._utc(_datetime_to_utc_tuple(datetime))

    def from_datetimes(self, datetime_list):
        """Return a `Time` for a list of Python ``datetime`` objects.

        The ``datetime`` objects must each be “timezone-aware”: they
        must each have a time zone object as their ``tzinfo`` attribute
        instead of ``None``.

        .. versionadded:: 1.24

        """
        tuples = (_datetime_to_utc_tuple(d) for d in datetime_list)
        return self._utc(array(value) for value in zip(*tuples))

    def utc(self, year, month=1, day=1, hour=0, minute=0, second=0.0):
        """Build a `Time` from a UTC `calendar date`.

        .. versionadded:: 1.24
           Passing a Python ``datetime`` or a list of datetimes as the
           first argument has been deprecated (and was never supported
           for the other time scale methods).  Instead, use the methods
           :meth:`~skyfield.timelib.Timescale.from_datetime()` and
           :meth:`~skyfield.timelib.Timescale.from_datetimes()`.

        """
        # TODO: someday deprecate passing datetime objects here, as
        # there are now separate constructors for them.
        if isinstance(year, datetime):
            return self.from_datetime(year)
        if isinstance(year, date):
            return self.from_datetime(datetime.combine(year, _time_zero))
        if hasattr(year, '__len__') and isinstance(year[0], datetime):
            return self.from_datetimes(year)

        a = _to_array
        tup = a(year), a(month), a(day), a(hour), a(minute), a(second)
        return self._utc(tup)

    def _utc(self, tup):
        # Build a Time from a UTC tuple, carefully preserving its exact
        # second number in the Time's hidden TAI seconds field.
        year, month, day, hour, minute, second = tup
        cutoff = self.julian_calendar_cutoff

        # Figure out exactly the TAI second number.
        seconds = (julian_day(year, month, day, cutoff) - 0.5) * DAY_S
        seconds, sfr = divmod(seconds, 1.0)  # in case there were any fractions
        seconds += interp(seconds, self._leap_utc, self._leap_offsets)
        more = hour * 3600.0 + minute * 60.0 + second
        seconds2, sfr = divmod(sfr + more, 1.0)
        seconds += seconds2

        # For the other timescales, use the usual Julian date + fraction.
        whole, fraction = divmod(seconds, DAY_S)
        fraction += sfr
        fraction /= DAY_S

        t = Time(self, whole, fraction + tt_minus_tai)
        t.tai_fraction = fraction
        t._tai_seconds = seconds, sfr
        return t

    def _jd(self, year, month, day, hour, minute, second):
        a = _to_array
        cutoff = self.julian_calendar_cutoff
        whole = julian_day(a(year), a(month), a(day), cutoff) - 0.5
        fraction = (a(second) + a(minute) * 60.0 + a(hour) * 3600.0) / DAY_S
        return _reconcile(whole, fraction)

    def _cal(self, whole, fraction):
        return calendar_tuple(whole, fraction, self.julian_calendar_cutoff)

    def _strftime(self, format, jd, fraction, seconds_bump=None):
        # Python forces an unhappy choice upon us: either use the faster
        # time.strftime() and lose support for '%f', or use the slower
        # datetime.strftime() and crash if years are negative.  We take the
        # first option, but then patch '%f' support back in by secretly
        # passing the microseconds string as the time zone name.  After all,
        # the routines supported by this function never use time zones.
        # What could go wrong?

        offset, ms = _strftime_offset_seconds(format)
        fraction = fraction + offset / DAY_S
        year, month, day, hour, minute, second = self._cal(jd, fraction)
        z = year * 0

        # TODO: will this always produce the same whole number that
        # calendar_tuple() produces internally?  Or should we make a private
        # version of calendar_tuple() that returns it to us for use here?
        weekday = (fraction + 0.5 + _to_array(jd)).astype(int) % 7

        if _format_uses_day_of_year(format):
            start_of_year = julian_day(year, 1, 1, self.julian_calendar_cutoff)
            yday = (jd + fraction + 1.5 - start_of_year).astype(int)
        else:
            yday = z

        if ms:
            format = format[:ms.start()] + '%Z' + format[ms.end():]
            second = (second * 1e6).astype(int)
            second, usec = divmod(second, 1000000)
            if seconds_bump is not None:
                second += seconds_bump
            if getattr(jd, 'ndim', 0):
                u = ['%06d' % u for u in usec]
                tup = (year, month, day, hour, minute, second,
                       weekday, yday, z, u)
                return [strftime(format, struct_time(t)) for t in zip(*tup)]
            u = '%06d' % usec
            tup = year, month, day, hour, minute, second, weekday, yday, z, u
            return strftime(format, struct_time(tup))
        else:
            second = second.astype(int)
            if seconds_bump is not None:
                second += seconds_bump
            tup = year, month, day, hour, minute, second, weekday, yday, z
            if getattr(jd, 'ndim', 0):
                return [strftime(format, item) for item in zip(*tup)]
            return strftime(format, tup)

    def _utc_jd(self, whole, fraction):
        # Switch from days to seconds.
        seconds = whole * DAY_S
        seconds2, fraction_s = divmod(fraction * DAY_S, 1.0)
        seconds += seconds2

        # Add an integer number of leap seconds.
        seconds += interp(seconds, self._leap_utc, self._leap_offsets)

        # Switch back to days.
        whole, fraction = divmod(seconds, DAY_S)
        fraction += fraction_s
        fraction /= DAY_S
        return self.tai_jd(whole, fraction)

    def tai(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
            jd=None):
        """Build a `Time` from an International Atomic Time `calendar date`.

        .. versionadded:: 1.6
           Passing a Julian date with ``jd=`` has been deprecated;
           instead, use :meth:`~skyfield.timelib.Timescale.tai_jd()`.

        """
        if jd is not None:
            return self.tai_jd(jd)  # deprecate someday
        whole, fraction = self._jd(year, month, day, hour, minute, second)
        t = Time(self, whole, fraction + tt_minus_tai)
        t.tai_fraction = fraction
        return t

    def tai_jd(self, jd, fraction=None):
        """Build a `Time` from an International Atomic Time Julian date."""
        jd, fraction = _normalize_jd_and_fraction(jd, fraction)
        t = Time(self, jd, fraction + tt_minus_tai)
        t.tai_fraction = fraction
        return t

    def tt(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
           jd=None):
        """Build a `Time` from a Terrestrial Time `calendar date`.

        .. versionadded:: 1.6
           Passing a Julian date with ``jd=`` has been deprecated;
           instead, use :meth:`~skyfield.timelib.Timescale.tt_jd()`.

        """
        if jd is not None:
            return self.tt_jd(jd)  # deprecate someday
        whole, fraction = self._jd(year, month, day, hour, minute, second)
        return Time(self, whole, fraction)

    def tt_jd(self, jd, fraction=None):
        """Build a `Time` from a Terrestrial Time Julian date."""
        jd, fraction = _normalize_jd_and_fraction(jd, fraction)
        return Time(self, jd, fraction)

    def J(self, year):
        """Build a `Time` from a Terrestrial Time Julian year or array.

        Julian years are convenient uniform periods of exactly 365.25
        days of Terrestrial Time, centered on 2000 January 1 12h TT =
        Julian year 2000.0.

        """
        tt = _to_array(year) * 365.25 + 1721045.0
        return Time(self, tt, 0.0)

    def tdb(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
            jd=None):
        """Build a `Time` from a Barycentric Dynamical Time `calendar date`.

        .. versionadded:: 1.6
           Passing a Julian date with ``jd=`` has been deprecated;
           instead, use :meth:`~skyfield.timelib.Timescale.tdb_jd()`.

        """
        if jd is not None:
            return self.tdb_jd(jd)  # deprecate someday
        whole, fraction = self._jd(year, month, day, hour, minute, second)
        jd = whole + fraction  # TODO: why do tests break if we pass separately
        return Time(self, jd, - tdb_minus_tt(jd) / DAY_S)

    def tdb_jd(self, jd, fraction=None):
        """Build a `Time` from a Barycentric Dynamical Time Julian date."""
        jd, fraction = _normalize_jd_and_fraction(jd, fraction)
        t = Time(self, jd, fraction - tdb_minus_tt(jd, fraction) / DAY_S)
        t.tdb_fraction = fraction
        return t

    def ut1(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
            jd=None):
        """Build a `Time` from a UT1 Universal Time `calendar date`.

        .. versionadded:: 1.6
           Passing a Julian date with ``jd=`` has been deprecated;
           instead, use :meth:`~skyfield.timelib.Timescale.ut1_jd()`.

        """
        if jd is None:  # TODO: deprecate the jd parameter to this method
            whole, fraction = self._jd(year, month, day, hour, minute, second)
            jd = whole + fraction  # TODO: can we pass high precision on?
        return self.ut1_jd(jd)

    def ut1_jd(self, jd):
        """Build a `Time` from a UT1 Universal Time Julian date."""
        ut1 = _to_array(jd)

        # Estimate TT = UT1, to get a rough Delta T estimate.
        tt_approx = ut1
        delta_t_approx = self.delta_t_function(tt_approx)

        # Use the rough Delta T to make a much better estimate of TT,
        # then generate an even better Delta T.
        tt_approx = ut1 + delta_t_approx / DAY_S
        delta_t_approx = self.delta_t_function(tt_approx)

        # We can now estimate TT with an error of < 1e-9 seconds within
        # 10 centuries of either side of the present; for details, see:
        # https://github.com/skyfielders/astronomy-notebooks
        # and look for the notebook "error-in-timescale-ut1.ipynb".
        delta_t_approx /= DAY_S
        t = Time(self, ut1, delta_t_approx)
        t.ut1_fraction = 0.0 * ut1
        return t

    def from_astropy(self, t):
        """Build a Skyfield `Time` from an AstroPy time object."""
        return self.tt(jd=t.tt.jd)

    def linspace(self, t0, t1, num=50):
        """Return ``num`` times spaced uniformly between ``t0`` to ``t1``.

        This routine is named after, and powered by, the NumPy routine
        `linspace()`_.

        .. _linspace(): https://numpy.org/doc/stable/reference/generated/numpy.linspace.html

        """
        whole0 = t0.whole
        frac0 = t0.tt_fraction
        whole1 = t1.whole
        frac1 = t1.tt_fraction
        return Time(
            self,
            linspace(whole0, whole1, num),
            linspace(frac0, frac1, num),
        )

class Time(object):
    """A single moment in history, or an array of several moments.

    Skyfield programs don’t usually instantiate this class directly, but
    instead build time objects using one of the timescale methods listed
    at `timescale-summary`.  If you do attempt the low-level operation
    of building a time object yourself, either leave ``tt_fraction`` at
    its default value of ``None`` — in which case Skyfield will assume
    the fraction is zero — or provide a ``tt_fraction`` array that has
    exactly the same dimensions as your ``tt`` array.

    """
    def __init__(self, ts, tt, tt_fraction=None):
        if tt_fraction is None:
            tt_fraction = zeros_like(tt)
        self.ts = ts
        self.whole = tt
        self.tt_fraction = tt_fraction
        self.shape = getattr(tt, 'shape', _EMPTY_TUPLE)

    def __len__(self):
        shape = self.shape
        if shape is _EMPTY_TUPLE:
            raise TypeError('this is a single Time, not an array')
        return shape[0]

    def __repr__(self):
        size = getattr(self.tt, 'size', -1)
        if size > 3:
            rstr = '<Time tt=[{0} ... {1}] len={2}>'
            return rstr.format(self.tt[0], self.tt[-1], size)
        else:
            return ('<Time tt={0}>'.format(self.tt)
                    .replace('[ ', '[').replace('  ', ' '))

    def __getitem__(self, index):
        if self.shape is _EMPTY_TUPLE:
            raise TypeError('this is a single Time, not an array')
        # TODO: also copy cached matrices?
        t = Time(self.ts, self.whole[index], self.tt_fraction[index])
        d = self.__dict__
        for name in 'tai_fraction', 'tdb_fraction', 'ut1_fraction':
            value = d.get(name)
            if value is not None:
                setattr(t, name, value[index])
        return t

    def astimezone(self, tz):
        """Convert to a Python ``datetime`` in a particular timezone ``tz``.

        If this time is an array, then an array of datetimes is returned
        instead of a single value.

        """
        dt, leap_second = self.astimezone_and_leap_second(tz)
        return dt

    def astimezone_and_leap_second(self, tz):
        """Convert to a Python ``datetime`` and leap second in a timezone.

        Convert this time to a Python ``datetime`` and a leap second::

            dt, leap_second = t.astimezone_and_leap_second(tz)

        The argument ``tz`` should be a ``datetime`` compatible
        timezone.

        The leap second value is provided because a Python ``datetime``
        can only number seconds ``0`` through ``59``, but leap seconds
        have a designation of at least ``60``.  The leap second return
        value will normally be ``0``, but will instead be ``1`` if the
        date and time are a UTC leap second.  Add the leap second value
        to the ``second`` field of the ``datetime`` to learn the real
        name of the second.

        If this time is an array, then an array of ``datetime`` objects
        and an array of leap second integers is returned, instead of a
        single value each.

        """
        dt, leap_second = self.utc_datetime_and_leap_second()
        normalize = getattr(tz, 'normalize', None)
        if self.shape and normalize is not None:
            dt = array([normalize(d.astimezone(tz)) for d in dt])
        elif self.shape:
            dt = array([d.astimezone(tz) for d in dt])
        elif normalize is not None:
            dt = normalize(dt.astimezone(tz))
        else:
            dt = dt.astimezone(tz)
        return dt, leap_second

    def toordinal(self):
        """Return the proleptic Gregorian ordinal of the UTC date.

        This method makes Skyfield `Time` objects compatible with Python
        `datetime`_ objects, which also provide a ``toordinal()``
        method.  Thanks to this method, a `Time` can often be used
        directly as a coordinate for a plot.

        """
        whole, fraction, is_leap_second = self._utc_seconds(0.0)
        return (whole - 148731076800.0 + fraction) / DAY_S

    def utc_datetime(self):
        """Convert to a Python ``datetime`` in UTC.

        If this time is an array, then a list of datetimes is returned
        instead of a single value.

        """
        dt, leap_second = self.utc_datetime_and_leap_second()
        return dt

    def utc_datetime_and_leap_second(self):
        """Convert to a Python ``datetime`` in UTC, plus a leap second value.

        Convert this time to a `datetime`_ object and a leap second::

            dt, leap_second = t.utc_datetime_and_leap_second()

        The leap second value is provided because a Python ``datetime``
        can only number seconds ``0`` through ``59``, but leap seconds
        have a designation of at least ``60``.  The leap second return
        value will normally be ``0``, but will instead be ``1`` if the
        date and time are a UTC leap second.  Add the leap second value
        to the ``second`` field of the ``datetime`` to learn the real
        name of the second.

        If this time is an array, then an array of ``datetime`` objects
        and an array of leap second integers is returned, instead of a
        single value each.

        """
        year, month, day, hour, minute, second = self._utc_tuple(0.5e-6)
        micro = (second * 1e6).astype(int)
        second, micro = divmod(micro, 1000000)
        leap_second = second // 60
        second -= leap_second  # fit within limited bounds of Python datetime
        if self.shape:
            zone = [utc] * self.shape[0]
            argsets = zip(year, month, day, hour, minute, second, micro, zone)
            d = []
            a = d.append
            try:  # placed outside the loop for efficiency
                for args in argsets:
                    a(datetime(*args))
            except ValueError as e:
                _upgrade_datetime_exception(args, e)
                raise
            d = array(d)
        else:
            args = year, month, day, hour, minute, second, micro, utc
            try:
                d = datetime(*args)
            except ValueError as e:
                _upgrade_datetime_exception(args, e)
                raise
        return d, leap_second

    def utc_iso(self, delimiter='T', places=0):
        """Convert to an ISO 8601 string like ``2014-01-18T01:35:38Z`` in UTC.

        If this time is an array of dates, then a sequence of strings is
        returned instead of a single string.

        """
        # "places" used to be the 1st argument, so continue to allow an
        # integer in that spot.  TODO: deprecate this in Skyfield 2.0
        # and remove it in 3.0.
        if isinstance(delimiter, int):
            places = delimiter
            delimiter = 'T'

        if places:
            power_of_ten = 10 ** places
            offset = 0.5 / power_of_ten
            year, month, day, hour, minute, second = self._utc_tuple(offset)
            second, fraction = divmod(second, 1.0)
            fraction *= power_of_ten
            format = '%04d-%02d-%02d{0}%02d:%02d:%02d.%0{1}dZ'.format(
                delimiter, places)
            args = (year, month, day, hour, minute, second, fraction)
        else:
            format = '%04d-%02d-%02d{0}%02d:%02d:%02dZ'.format(delimiter)
            args = self._utc_tuple(0.5)

        if self.shape:
            return [format % tup for tup in zip(*args)]
        else:
            return format % args

    def utc_jpl(self):
        """Convert to a string like ``A.D. 2014-Jan-18 01:35:37.5000 UTC``.

        Returns a string for this date and time in UTC, in the format
        used by the JPL HORIZONS system.  If this time is an array of
        dates, then a sequence of strings is returned instead of a
        single string.

        """
        year, month, day, hour, minute, second = self._utc_tuple(0.00005)
        second, fraction = divmod(second, 1.0)
        fraction *= 1e4
        bc = year < 1
        year = abs(year - bc)
        era = where(bc, 'B.C.', 'A.D.')
        format = '%s %04d-%s-%02d %02d:%02d:%02d.%04d UTC'
        args = (era, year, MONTH_NAMES[month], day,
                hour, minute, second, fraction)

        if self.shape:
            return [format % tup for tup in zip(*args)]
        else:
            return format % args

    def utc_strftime(self, format='%Y-%m-%d %H:%M:%S UTC'):
        """Format the UTC time using a Python datetime formatting string.

        This calls Python’s ``time.strftime()`` to format the date and
        time.  A single string is returned or else a whole array of
        strings, depending on whether this time object is an array.
        The most commonly used formats are:

        * ``%Y`` four-digit year, ``%y`` two-digit year
        * ``%m`` month number, ``%B`` name, ``%b`` abbreviation
        * ``%d`` day of month
        * ``%H`` hour
        * ``%M`` minute
        * ``%S`` second
        * ``%A`` day of week, ``%a`` its abbreviation

        The ``%Z`` and ``%z`` formats are not supported; instead, simply
        use the literal characters ``'UTC'`` in your format string.

        If the smallest time unit in your format is minutes or seconds,
        then the time is rounded to the nearest minute or second.
        Otherwise the value is truncated rather than rounded.

        """
        offset, uses_ms = _strftime_offset_seconds(format)
        year, month, day, hour, minute, second, jd = self._utc_tuple(offset, 1)
        start_of_year = julian_day(year, 1, 1, self.ts.julian_calendar_cutoff)
        weekday = jd % 7
        yday = jd + 1 - start_of_year
        return _strftime(format, year, month, day, hour, minute, second,
                         weekday, yday, uses_ms)

    def _utc_tuple(self, offset, return_jd=False):
        """Return UTC as (year, month, day, hour, minute, second.fraction).

        The `offset` in seconds is added to the UTC time before it is
        split into its components.  This is useful if the user is going
        to round the result before displaying it.  If the result is
        going to be displayed as seconds, for example, set `offset` to
        0.5 and then throw away the fraction; if the result is going to
        be displayed as minutes, set `offset` to 30.0 and then throw
        away the seconds; and so forth.

        """
        second, sfr, is_leap_second = self._utc_seconds(offset)
        second = second.astype(int64)
        second -= is_leap_second
        jd, second = divmod(second + 43200, 86400)
        cutoff = self.ts.julian_calendar_cutoff
        year, month, day = compute_calendar_date(jd, cutoff)
        minute, second = divmod(second, 60)
        hour, minute = divmod(minute, 60)
        second += is_leap_second
        if not return_jd:
            return year, month, day, hour, minute, second + sfr
        return year, month, day, hour, minute, second + sfr, jd

    def _utc_seconds(self, offset):
        """Return integer seconds since JD 0.0, plus a 0 ≤ fraction < 1."""
        seconds, fr = self._tai_seconds
        seconds2, fr = divmod(fr + offset, 1.0)
        seconds = seconds + seconds2  # not +=, which would modify cached array
        ts = self.ts
        tai_minus_utc = interp(seconds, ts._leap_tai, ts._leap_offsets)
        tai_minus_utc, is_leap_second = divmod(tai_minus_utc, 1.0)
        is_leap_second = is_leap_second > 0.0
        return seconds - tai_minus_utc, fr, is_leap_second

    @reify
    def _tai_seconds(self):
        # If UTC was supplied, this will already hold an exact value; otherwise:
        seconds, fr = divmod(self.whole * DAY_S, 1.0)
        seconds2, fr = divmod(fr + self.tai_fraction * DAY_S, 1.0)
        seconds += seconds2
        return seconds, fr

    def _leap_seconds(self):
        # TODO: should this be reified?
        ts = self.ts
        seconds, fr = self._tai_seconds
        return interp(seconds, ts._leap_tai, ts._leap_offsets)

    # Calendar tuples.

    def tai_calendar(self):
        """TAI as a (year, month, day, hour, minute, second) `calendar date`."""
        return self.ts._cal(self.whole, self.tai_fraction)

    def tt_calendar(self):
        """TT as a (year, month, day, hour, minute, second) `calendar date`."""
        return self.ts._cal(self.whole, self.tt_fraction)

    def tdb_calendar(self):
        """TDB as a (year, month, day, hour, minute, second) `calendar date`."""
        return self.ts._cal(self.whole, self.tdb_fraction)

    def ut1_calendar(self):
        """UT1 as a (year, month, day, hour, minute, second) `calendar date`."""
        return self.ts._cal(self.whole, self.ut1_fraction)

    # Date formatting.

    def tai_strftime(self, format='%Y-%m-%d %H:%M:%S TAI'):
        """Format TAI with a datetime strftime() format string."""
        return self.ts._strftime(format, self.whole, self.tai_fraction)

    def tt_strftime(self, format='%Y-%m-%d %H:%M:%S TT'):
        """Format TT with a datetime strftime() format string."""
        return self.ts._strftime(format, self.whole, self.tt_fraction)

    def tdb_strftime(self, format='%Y-%m-%d %H:%M:%S TDB'):
        """Format TDB with a datetime strftime() format string."""
        return self.ts._strftime(format, self.whole, self.tdb_fraction)

    def ut1_strftime(self, format='%Y-%m-%d %H:%M:%S UT1'):
        """Format UT1 with a datetime strftime() format string."""
        return self.ts._strftime(format, self.whole, self.ut1_fraction)

    # Convenient caching of several expensive functions of time.

    @reify
    def M(self):
        """3×3 rotation matrix: ICRS → equinox of this date."""

        # Compute N and P instead of asking for self.N and self.P to
        # avoid keeping copies of them, since Skyfield itself never uses
        # them again once M has been computed.  But we do check in case
        # a user has forced them to be built already.

        d = self.__dict__

        P = d.get('P')
        if P is None:
            P = self.precession_matrix()

        N = d.get('N')
        if N is None:
            N = self.nutation_matrix()

        return mxmxm(N, P, B)

    @reify
    def MT(self):
        """3×3 rotation matrix: equinox of this date → ICRS."""
        return rollaxis(self.M, 1)

    @reify
    def C(self):
        # Calculate the Equation of Origins in cycles
        eq_origins = (earth_rotation_angle(self.ut1) - self.gast / 24.0)
        R = rot_z(2 * pi * eq_origins)
        return mxm(R, self.M)

    @reify
    def CT(self):
        return rollaxis(self.C, 1)

    @reify
    def _nutation_angles_radians(self):
        # TODO: add psi and eps corrections support back in here, rather
        # than at points of use.
        return iau2000a_radians(self)

    def _nutation_angles(self, angles):
        # Sample code shared with early adopters suggested that setting
        # this attribute manually could avoid the expense of IAU 2000A,
        # so this setter continues to support the pattern.

        d_psi, d_eps = angles
        self._nutation_angles_radians = (
            d_psi / 1e7 * ASEC2RAD,
            d_eps / 1e7 * ASEC2RAD,
        )

    _nutation_angles = property(None, _nutation_angles)

    @reify
    def _mean_obliquity_radians(self):
        # Cached because it is used to compute both gast and N.
        return mean_obliquity(self.tdb) * ASEC2RAD

    # Conversion between timescales.

    @reify
    def J(self):
        """Return a floating point Julian year or array of years for this date.

        Julian years are convenient uniform periods of exactly 365.25
        days of Terrestrial Time, centered on 2000 January 1 12h TT =
        Julian year 2000.0.

        """
        return (self.whole - 1721045.0 + self.tt_fraction) / 365.25

    @reify
    def utc(self):
        """A tuple ``(year, month, day, hour, minute, second)`` in UTC."""
        utc = self._utc_tuple(0.0)
        return (array(utc).view(CalendarArray) if self.shape
                else CalendarTuple(*utc))

    @reify
    def tai_fraction(self):
        return self.tt_fraction - tt_minus_tai

    @reify
    def tdb_fraction(self):
        fr = self.tt_fraction
        return fr + tdb_minus_tt(self.whole, fr) / DAY_S

    @reify
    def ut1_fraction(self):
        return self.tt_fraction - self.delta_t / DAY_S

    @reify
    def delta_t(self):
        return self.ts.delta_t_function(self.tt)

    @reify
    def dut1(self):
        return 32.184 + self._leap_seconds() - self.delta_t

    @reify
    def gmst(self):
        """Greenwich Mean Sidereal Time (GMST) in hours."""
        return sidereal_time(self)

    @reify
    def gast(self):
        """Greenwich Apparent Sidereal Time (GAST) in hours."""
        d_psi, _ = self._nutation_angles_radians
        tt = self.tt
        # TODO: move this into an eqeq function?
        c_terms = equation_of_the_equinoxes_complimentary_terms(tt)
        eq_eq = d_psi * cos(self._mean_obliquity_radians) + c_terms
        return (self.gmst + eq_eq / tau * 24.0) % 24.0

    # Low-precision floats generated from internal float pairs.

    @property
    def tai(self):
        return self.whole + self.tai_fraction

    @property
    def tt(self):
        return self.whole + self.tt_fraction

    @property
    def tdb(self):
        return self.whole + self.tdb_fraction

    @property
    def ut1(self):
        return self.whole + self.ut1_fraction

    # Earth position as a function of time.

    def polar_motion_angles(self):
        table = self.ts.polar_motion_table
        if table is None:
            return 0.0, 0.0, 0.0
        sprime = -47.0e-6 * (self.tdb - T0) / 36525.0
        tt, x, y = table
        return sprime, interp(self.tt, tt, x), interp(self.tt, tt, y)

    def polar_motion_matrix(self):
        sprime, x, y = self.polar_motion_angles()
        return mxmxm(
            rot_x(y * ASEC2RAD),
            rot_y(x * ASEC2RAD),
            rot_z(-sprime * ASEC2RAD),
        )

    def nutation_matrix(self):
        """Compute the 3×3 nutation matrix N for this date."""
        d_psi, d_eps = self._nutation_angles_radians
        mean_obliquity = self._mean_obliquity_radians
        true_obliquity = mean_obliquity + d_eps
        return build_nutation_matrix(mean_obliquity, true_obliquity, d_psi)

    def precession_matrix(self):
        """Compute the 3×3 precession matrix P for this date."""
        return compute_precession(self.tdb)

    # Various dunders.

    def __eq__(self, other_time):
        if isinstance(other_time, Time):
            return self.__sub__(other_time) == 0.0
        return False

    def __lt__(self, other):
        return ((self.tt_fraction - other.tt_fraction)
                + (self.whole - other.whole)) < 0

    def __add__(self, other_time):
        if isinstance(other_time, timedelta):
            w = other_time.days
            f = other_time.seconds / DAY_S + other_time.microseconds / DAY_US
        elif isinstance(other_time, (int, float, ndarray)):
            w, f = divmod(other_time, 1.0)
        else:
            return NotImplemented

        return self.ts.tt_jd(self.whole + w, self.tt_fraction + f)

    def __sub__(self, other_time):
        if isinstance(other_time, Time):
            return self.whole - other_time.whole + (
                self.tt_fraction - other_time.tt_fraction
            )
        elif isinstance(other_time, timedelta):
            w = other_time.days
            f = other_time.seconds / DAY_S + other_time.microseconds / DAY_US
        elif isinstance(other_time, (int, float, ndarray)):
            w, f = divmod(other_time, 1.0)
        else:
            return NotImplemented

        return self.ts.tt_jd(self.whole - w, self.tt_fraction - f)

    def __hash__(self):
        # Someone wanted to use Time objects with functools.lru_cache so
        # we make this attempt to support hashability; beware that it
        # will return the same hash for very closely spaced times that
        # all round to the same floating point TT.
        return hash(self.tt)

    # Interoperability.

    def to_astropy(self):
        """Return an AstroPy object representing this time."""
        from astropy.time import Time
        return Time(self.whole, self.tt_fraction, format='jd', scale='tt')

    # Deprecated attributes that were once used internally, consuming
    # memory with matrices that are never used again by Skyfield once
    # t.M has been computed.

    P = reify(precession_matrix)
    N = reify(nutation_matrix)
    P.__doc__ = N.__doc__ = None  # omit from Sphinx documentation

    @reify
    def PT(self): return rollaxis(self.P, 1)
    @reify
    def NT(self): return rollaxis(self.N, 1)

def julian_day(year, month=1, day=1, julian_before=None):
    """Given a calendar date, return a Julian day integer.

    Uses the proleptic Gregorian calendar unless ``julian_before`` is
    set to a specific Julian day, in which case the Julian calendar is
    used for dates older than that.

    """
    # Support months <1 and >12 by overflowing cleanly into adjacent years.
    y, month = divmod(month - 1, 12)
    year = year + y
    month += 1

    # See the Explanatory Supplement to the Astronomical Almanac 15.11.
    janfeb = month <= 2
    g = year + 4716 - janfeb
    f = (month + 9) % 12
    e = 1461 * g // 4 + day - 1402
    J = e + (153 * f + 2) // 5

    mask = 1 if (julian_before is None) else (J >= julian_before)
    J += (38 - (g + 184) // 100 * 3 // 4) * mask
    return J

def julian_date(year, month=1, day=1, hour=0, minute=0, second=0.0):
    """Given a proleptic Gregorian calendar date and time, build a Julian date.

    The difference between a “Julian day” and a “Julian date” is that
    the “day” is the integer part, while the “date” includes a fraction
    indicating the time.

    """
    return julian_day(year, month, day) - 0.5 + (
        second + minute * 60.0 + hour * 3600.0) / DAY_S

def julian_date_of_besselian_epoch(b):
    return 2415020.31352 + (b - 1900.0) * 365.242198781

def compute_calendar_date(jd_integer, julian_before=None):
    """Convert Julian day ``jd_integer`` into a calendar (year, month, day).

    Uses the proleptic Gregorian calendar unless ``julian_before`` is
    set to a specific Julian day, in which case the Julian calendar is
    used for dates older than that.

    """
    use_gregorian = (julian_before is None) or (jd_integer >= julian_before)

    # See the Explanatory Supplement to the Astronomical Almanac 15.11.
    f = jd_integer + 1401
    f += use_gregorian * ((4 * jd_integer + 274277) // 146097 * 3 // 4 - 38)
    e = 4 * f + 3
    g = e % 1461 // 4
    h = 5 * g + 2
    day = h % 153 // 5 + 1
    month = (h // 153 + 2) % 12 + 1
    year = e // 1461 - 4716 + (12 + 2 - month) // 12
    return year, month, day

calendar_date = compute_calendar_date  # old name, in case anyone used it

def calendar_tuple(jd_float, fraction=0.0, julian_before=None):
    """Return a (year, month, day, hour, minute, second.fraction) tuple."""
    jd_float = _to_array(jd_float)
    whole1, fraction1 = divmod(jd_float, 1.0)
    whole2, fraction = divmod(fraction1 + fraction + 0.5, 1.0)
    whole = (whole1 + whole2).astype(int)
    year, month, day = compute_calendar_date(whole, julian_before)
    second = fraction * 86400.0
    minute, second = divmod(second, 60.0)
    minute = minute.astype(int)
    hour, minute = divmod(minute, 60)
    return year, month, day, hour, minute, second

def tdb_minus_tt(jd_tdb, fraction_tdb=0.0):
    """Computes how far TDB is in advance of TT, given TDB.

    Given that the two time scales never diverge by more than 2ms, TT
    can also be given as the argument to perform the conversion in the
    other direction.

    """
    t = (jd_tdb - T0 + fraction_tdb) / 36525.0

    # USNO Circular 179, eq. 2.6.
    return (0.001657 * sin ( 628.3076 * t + 6.2401)
          + 0.000022 * sin ( 575.3385 * t + 4.2970)
          + 0.000014 * sin (1256.6152 * t + 6.1969)
          + 0.000005 * sin ( 606.9777 * t + 4.0212)
          + 0.000005 * sin (  52.9691 * t + 0.4444)
          + 0.000002 * sin (  21.3299 * t + 5.5431)
          + 0.000010 * t * sin ( 628.3076 * t + 4.2490))

class DeltaT(object):
    def __init__(self, table_tt, table_delta_t, long_term_function):
        self.table_tt = table_tt
        self.table_delta_t = table_delta_t
        self.long_term_function = long_term_function

    def __call__(self, tt):
        delta_t = interp(tt, self.table_tt, self.table_delta_t, nan, nan)
        if getattr(delta_t, 'shape', None):
            nan_indexes = nonzero(isnan(delta_t))
            if nan_indexes:
                J = (tt[nan_indexes] - 1721045.0) / 365.25
                delta_t[nan_indexes] = self.long_term_function(J)
        else:
            if isnan(delta_t):
                J = (tt - 1721045.0) / 365.25
                delta_t = self.long_term_function(J)
        return delta_t

delta_t_parabola_stephenson_morrison_hohenkerk_2016 = Splines(
    [1825.0, 1925.0, 0.0, 32.5, 0.0, -320.0])

delta_t_parabola_morrison_stephenson_2004 = Splines(
    [1820.0, 1920.0, 0.0, 32.0, 0.0, -20.0])

def build_delta_t(delta_t_recent):
    """Return a function t→∆T, given recent real-world observations of ∆T.

    When asked for ∆T outside the range of the `delta_t_recent` table,
    Skyfield uses the splines for 720 BC – AD 2015 computed by Morrison,
    Stephenson, Hohenkerk, and Zawilski.  For dates that fall outside of
    the spines, we use the long-term parabola of Stephenson, Morrison,
    and Hohenkerk.

    """
    parabola = delta_t_parabola_stephenson_morrison_hohenkerk_2016
    s15_table = load_bundled_npy('delta_t.npz')['Table-S15.2020.txt']
    table_tt, table_delta_t = delta_t_recent

    p = parabola
    pd = parabola.derivative
    s = Splines(s15_table)
    sd = s.derivative

    long_term_parabola_width = p.upper[0] - p.lower[0]

    # How many years wide we make the splines that connect the tables to
    # the long-term parabola; tuned by hand until the derivative graphed
    # by `work_on_delta_t_discontinuities()` in `design/delta_t.py`
    # doesn't look too terrible.
    patch_width = 800.0

    # To the left of the Table-S15 splines, design a spline connecting
    # them to the long-term parabola.

    x1 = s.lower[0]  # For the current table, this = -720.0.
    x0 = x1 - patch_width
    left = build_spline_given_ends(x0, p(x0), pd(x0), x1, s(x1), sd(x1))

    # And to the left of that, put the pure long-term parabola.

    x1 = x0
    x0 = x1 - long_term_parabola_width
    far_left = build_spline_given_ends(x0, p(x0), pd(x0), x1, p(x1), pd(x1))

    # Truncate the splines table where the daily table starts, and
    # adjust the final spline to remove any discontinuity.

    x = (table_tt[0] - 1721045.0) / 365.25  # TT to J centuries

    i = searchsorted(s15_table[0], x)
    s15_table = s15_table[:,:i]

    desired_y = table_delta_t[0]
    current_y = s(x)
    x0, x1, a3, a2, a1, a0 = s15_table[:,-1]
    t = (x - x0) / (x1 - x0)
    a1 = a1 + (desired_y - current_y) / t  # adjust linear term
    s15_table[:,-1] = x0, x1, a3, a2, a1, a0

    # To the right of the recent ∆T table, design a spline connecting
    # smoothly to the long-term parabola.

    x0 = (table_tt[-1] - 1721045.0) / 365.25  # TT to J centuries
    x1 = (x0 + patch_width) // 100.0 * 100.0  # Choose multiple of 100 years
    y0 = table_delta_t[-1]

    lookback = min(366, len(table_delta_t))       # Slope of last year of ∆T.
    slope = (table_delta_t[-1] - table_delta_t[-lookback]) * lookback / 365.0
    right = build_spline_given_ends(x0, y0, slope, x1, p(x1), pd(x1))

    # At the far right, finish with the pure long-term parabola.

    x0 = x1
    x1 = x0 + long_term_parabola_width
    far_right = build_spline_given_ends(x0, p(x0), pd(x0), x1, p(x1), pd(x1))

    curve = Splines(_cat(
        array([far_left]).T,
        array([left]).T,
        s15_table,
        array([right]).T,
        array([far_right]).T,
    ))

    return DeltaT(table_tt, table_delta_t, curve)

def build_delta_t_table(delta_t_recent):
    """Deprecated: the Delta T interpolation table used in Skyfield <= 1.37."""
    bundled = _cat(
        load_bundled_npy('morrison_stephenson_deltat.npy')[:,:22],
        load_bundled_npy('historic_deltat.npy'),
    )
    recent_start_time = delta_t_recent[0,0]
    i = searchsorted(bundled[0], recent_start_time)
    century = 36524.0
    start_tt = bundled[0,0] - century
    start_J = Time(None, start_tt).J
    end_tt = delta_t_recent[0,-1] + century
    end_J = Time(None, end_tt).J
    return _cat(
        [[start_tt], [delta_t_parabola_morrison_stephenson_2004(start_J)]],
        bundled[:,:i],
        delta_t_recent,
        [[end_tt], [delta_t_parabola_morrison_stephenson_2004(end_J)]],
    )

_format_uses_milliseconds = re.compile(r'%[-_0^#EO]*f').search
_format_uses_seconds = re.compile(r'%[-_0^#EO]*[STXc]').search
_format_uses_minutes = re.compile(r'%[-_0^#EO]*[MR]').search
_format_uses_day_of_year = re.compile(r'%[-_0^#EO]*j').search

def _datetime_to_utc_tuple(dt):
    z = dt.tzinfo
    if z is None:
        raise ValueError(_naive_complaint)
    if z is not utc:
        dt = dt.astimezone(utc)
    return (dt.year, dt.month, dt.day,
            dt.hour, dt.minute, dt.second + dt.microsecond / 1e6)

def _normalize_jd_and_fraction(jd, fraction):
    jd = _to_array(jd)
    if fraction is None:
        jd, fraction = divmod(jd, 1.0)
    else:
        jd, fraction = _reconcile(jd, _to_array(fraction))
    return jd, fraction

def _strftime_offset_seconds(format):
    uses_ms = _format_uses_milliseconds(format)
    if uses_ms:
        if _OLD_PYTHON:
            raise ValueError('strftime() "%f" not supported under Python 2')
        offset = 1e-16  # encourage .0 to not turn into .999999
    elif _format_uses_seconds(format):
        offset = 0.5
    elif _format_uses_minutes(format):
        offset = 30.0
    else:
        offset = 0.0
    return offset, uses_ms

def _strftime(format, year, month, day, hour, minute, second,
              weekday, yday, uses_ms):
    zero = year * 0

    # TODO: avoid computing yday if this is false:
    #_format_uses_day_of_year(format)

    if uses_ms:
        format = format[:uses_ms.start()] + '%Z' + format[uses_ms.end():]
        second = (second * 1e6).astype(int)
        second, usec = divmod(second, 1000000)
        if getattr(year, 'ndim', 0):
            u = ['%06d' % u for u in usec]
            tup = (year, month, day, hour, minute, second,
                   weekday, yday, zero, u)
            return [strftime(format, struct_time(t)) for t in zip(*tup)]
        u = '%06d' % usec
        tup = year, month, day, hour, minute, second, weekday, yday, zero, u
        return strftime(format, struct_time(tup))
    else:
        second = second.astype(int)
        tup = year, month, day, hour, minute, second, weekday, yday, zero
        if getattr(year, 'ndim', 0):
            return [strftime(format, item) for item in zip(*tup)]
        return strftime(format, tup)

def _upgrade_datetime_exception(args, e):
    year, month, day, hour, minute, second, micro, zone = args
    if year < 0:
        e.args = ("Python's datetime does not support negative"
                  ' years like the year {}'.format(year),)
    elif month == 2 and day == 29:
        e.args = ("Python's datetime does not support Julian leap"
                  ' days like {} February 29 that are missing from'
                  ' the Gregorian calendar'.format(year),)

_naive_complaint = """cannot interpret a datetime that lacks a timezone

You must either specify that your datetime is in UTC:

    from skyfield.api import utc
    d = datetime(..., tzinfo=utc)  # to build a new datetime
    d = d.replace(tzinfo=utc)      # to fix an existing datetime

Or use a timezone object like those provided by the third-party `pytz` library:

    from pytz import timezone
    eastern = timezone('US/Eastern')
    d = eastern.localize(datetime(2014, 1, 16, 1, 32, 9))"""
