#!/usr/bin/env python
# coding=utf-8
"""Lunisolar calendar calculations.

Utilities for computing the dates of holidays based on lunar or solar events.

The definition for Chinese New Year comes from:
Aslaksen, Helmer. "The Mathematics of the Chinese Calendar",
http://www.math.nus.edu.sg/aslaksen/calendar/cal.pdf

This utility uses NOVAS (http://aa.usno.navy.mil/software/novas/novas_info.php)
to predict the position of the sun:
Barron, E. G., Kaplan, G. H., Bangert, J., Bartlett, J. L., Puatua, W., Harris,
W., & Barrett, P.  (2011) "Naval Observatory Vector Astrometry Software (NOVAS)
Version 3.1, Introducing a Python Edition," Bull. AAS, 43, 2011. (Abstract)

I also found the following book very helpful to understand the underlying
concepts for these predictions:
Jean H. Meeus. 1991. Astronomical Algorithms. Willmann-Bell, Incorporated.
"""
import contextlib
import operator as op
import pathlib
import re

import click
import novas.compat as novas
import numpy as np
import pandas as pd
import toolz
from novas.compat import eph_manager

from exchange_calendars.calendar_helpers import UTC


@contextlib.contextmanager
def ephemeris():
    """A context manager for managing the novas ephemeris file."""
    eph_manager.ephem_open()
    try:
        yield
    finally:
        eph_manager.ephem_close()


def load_precomputed_data(root_dir):
    """Load saved data.

    Parameters
    ----------
    root_dir : path-like
        The directory to load the data from.

    Returns
    -------
    values : np.ndarray[float64]
        The precomputed data.
    minutes : np.ndarray[M8[m]]
        The minute labels for the data.
    """
    root_dir = pathlib.Path(root_dir)

    pattern = re.compile(r"(.*)_(.*)")
    files = [(p, pattern.match(p.name)) for p in root_dir.iterdir()]
    files = list(filter(lambda t: t[1] is not None, files))
    files = sorted(files, key=lambda t: np.datetime64(t[1][1]))

    values = []
    minutes = []
    for path, match in files:
        values.append(np.fromfile(str(path)))
        minutes.append(
            np.arange(
                match[1],
                np.datetime64(match[2]) + np.timedelta64(1, "m"),
                dtype="M8[m]",
            )
        )

    return np.hstack(values), np.hstack(minutes)


def smooth(arr, window):
    """Smooth an array by taking a rolling arithmetic mean.

    Parameters
    ----------
    arr : np.ndarray
        The array to smooth.
    window : int
        The size of the window to use when taking the mean.

    Returns
    -------
    smoothed : np.ndarray
        The smoothed array of length ``len(arr) - window // 2``.
    """
    return np.convolve(np.ones(window) / window, arr, mode="valid")


def local_cmp(f, arr):
    """Compare elements of an array to its direct neighbors.

    Parameters
    ----------
    f : callable
        The comparison to apply.
    arr : np.ndarray
        The array to check.

    Returns
    -------
    ix : np.ndarray[int64]
        An array of indices where ``f(arr[i], arr[i + 1])`` and
        ``f(arr[i - 1], arr[i])`` are true. index 0 and -1 are considered
        False.

    Examples
    --------
    >>> import operator as op
    # 100 samples of a path of 5 rotations
    >>> rad = np.fmod(np.linspace(0, 5 * 2 * np.pi, 100), 2 * np.pi)
    >>> sin = np.sin(rad)
    >>> local_minima_ix = local_cmp(op.lt, sin)

    # NOTE: this is not all -1 because we only took 100 samples so
    # 3 * np.pi / 2 doesn't appear exactly in the ``rad`` array.
    >>> sin[local_minima_ix]
    array([-0.99886734, -0.99383846, -0.98982144, -0.99685478, -0.99987413])

    >>> rad[local_minima_ix]
    array([ 4.75998887,  4.82345539,  4.56958931,  4.63305583,  4.69652235])
    """
    return np.flatnonzero(
        np.r_[False, f(arr[1:], arr[:-1])] & np.r_[f(arr[:-1], arr[1:]), False]
    )


def smoothed_local_cmp(f, arr, window):
    """Smooth an array and then compare elements to its direct neighbors.

    Parameters
    ----------
    f : callable
        The comparison to apply.
    arr : np.ndarray
        The array to check.
    window : int
        The size of the window to use when smoothing the array.

    Returns
    -------
    ix : np.ndarray[int64]
        An array of indices where ``f(arr[i], arr[i + 1])`` and
        ``f(arr[i - 1], arr[i])`` are true. index 0 and -1 are considered
        False.
    """
    return local_cmp(f, smooth(arr, window)) + window // 2


def utc_to_jd_tt(ts):
    """Convert UTC datetimes to Julian Date Terrestrial Time.

    Parameters
    ----------
    ts : np.ndarray[M8[s]]
        The timestamps to convert.

    Returns
    -------
    jd_tt : np.ndarray[f8]
        The julian date terrestrial time for the UTC values.
    """
    # lookup the corrections stored on this function object
    corrections = utc_to_jd_tt.corrections

    jd_utc = ts.astype("M8[s]").view("i8") / 86400 + 2440587.5
    correction_ix = np.searchsorted(
        corrections["time"],
        ts,
        side="right",
    )
    correction_ix[correction_ix == len(corrections)] -= 1
    return jd_utc + corrections["julian_days"][correction_ix]


def T(year, month, day):
    return np.datetime64(f"{year}-{month:02d}-{day:02d}", "D")


# UTC to JD TT adjustment factors from:
# https://www.usno.navy.mil/USNO/earth-orientation/eo-products/long-term
utc_to_jd_tt.corrections = np.array(
    [
        (T(1700, 1, 1), 32.184),
        (T(1973, 2, 1), 43.4724),
        (T(1973, 3, 1), 43.5648),
        (T(1973, 4, 1), 43.6737),
        (T(1973, 5, 1), 43.7782),
        (T(1973, 6, 1), 43.8763),
        (T(1973, 7, 1), 43.9562),
        (T(1973, 8, 1), 44.0315),
        (T(1973, 9, 1), 44.1132),
        (T(1973, 10, 1), 44.1982),
        (T(1973, 11, 1), 44.2952),
        (T(1973, 12, 1), 44.3936),
        (T(1974, 1, 1), 44.4840),
        (T(1974, 2, 1), 44.5646),
        (T(1974, 3, 1), 44.6425),
        (T(1974, 4, 1), 44.7386),
        (T(1974, 5, 1), 44.8370),
        (T(1974, 6, 1), 44.9302),
        (T(1974, 7, 1), 44.9986),
        (T(1974, 8, 1), 45.0583),
        (T(1974, 9, 1), 45.1284),
        (T(1974, 10, 1), 45.2064),
        (T(1974, 11, 1), 45.2980),
        (T(1974, 12, 1), 45.3897),
        (T(1975, 1, 1), 45.4761),
        (T(1975, 2, 1), 45.5632),
        (T(1975, 3, 1), 45.6450),
        (T(1975, 4, 1), 45.7374),
        (T(1975, 5, 1), 45.8284),
        (T(1975, 6, 1), 45.9133),
        (T(1975, 7, 1), 45.9820),
        (T(1975, 8, 1), 46.0407),
        (T(1975, 9, 1), 46.1067),
        (T(1975, 10, 1), 46.1825),
        (T(1975, 11, 1), 46.2788),
        (T(1975, 12, 1), 46.3713),
        (T(1976, 1, 1), 46.4567),
        (T(1976, 2, 1), 46.5445),
        (T(1976, 3, 1), 46.6311),
        (T(1976, 4, 1), 46.7302),
        (T(1976, 5, 1), 46.8283),
        (T(1976, 6, 1), 46.9247),
        (T(1976, 7, 1), 46.9970),
        (T(1976, 8, 1), 47.0709),
        (T(1976, 9, 1), 47.1450),
        (T(1976, 10, 1), 47.2361),
        (T(1976, 11, 1), 47.3413),
        (T(1976, 12, 1), 47.4319),
        (T(1977, 1, 1), 47.5214),
        (T(1977, 2, 1), 47.6049),
        (T(1977, 3, 1), 47.6837),
        (T(1977, 4, 1), 47.7781),
        (T(1977, 5, 1), 47.8771),
        (T(1977, 6, 1), 47.9687),
        (T(1977, 7, 1), 48.0348),
        (T(1977, 8, 1), 48.0942),
        (T(1977, 9, 1), 48.1608),
        (T(1977, 10, 1), 48.2460),
        (T(1977, 11, 1), 48.3438),
        (T(1977, 12, 1), 48.4355),
        (T(1978, 1, 1), 48.5344),
        (T(1978, 2, 1), 48.6324),
        (T(1978, 3, 1), 48.7294),
        (T(1978, 4, 1), 48.8365),
        (T(1978, 5, 1), 48.9353),
        (T(1978, 6, 1), 49.0319),
        (T(1978, 7, 1), 49.1013),
        (T(1978, 8, 1), 49.1591),
        (T(1978, 9, 1), 49.2285),
        (T(1978, 10, 1), 49.3070),
        (T(1978, 11, 1), 49.4018),
        (T(1978, 12, 1), 49.4945),
        (T(1979, 1, 1), 49.5861),
        (T(1979, 2, 1), 49.6805),
        (T(1979, 3, 1), 49.7602),
        (T(1979, 4, 1), 49.8556),
        (T(1979, 5, 1), 49.9489),
        (T(1979, 6, 1), 50.0347),
        (T(1979, 7, 1), 50.1018),
        (T(1979, 8, 1), 50.1622),
        (T(1979, 9, 1), 50.2260),
        (T(1979, 10, 1), 50.2968),
        (T(1979, 11, 1), 50.3831),
        (T(1979, 12, 1), 50.4599),
        (T(1980, 1, 1), 50.5387),
        (T(1980, 2, 1), 50.6160),
        (T(1980, 3, 1), 50.6866),
        (T(1980, 4, 1), 50.7658),
        (T(1980, 5, 1), 50.8454),
        (T(1980, 6, 1), 50.9187),
        (T(1980, 7, 1), 50.9761),
        (T(1980, 8, 1), 51.0278),
        (T(1980, 9, 1), 51.0843),
        (T(1980, 10, 1), 51.1538),
        (T(1980, 11, 1), 51.2319),
        (T(1980, 12, 1), 51.3063),
        (T(1981, 1, 1), 51.3808),
        (T(1981, 2, 1), 51.4526),
        (T(1981, 3, 1), 51.5160),
        (T(1981, 4, 1), 51.5985),
        (T(1981, 5, 1), 51.6809),
        (T(1981, 6, 1), 51.7573),
        (T(1981, 7, 1), 51.8133),
        (T(1981, 8, 1), 51.8532),
        (T(1981, 9, 1), 51.9014),
        (T(1981, 10, 1), 51.9603),
        (T(1981, 11, 1), 52.0328),
        (T(1981, 12, 1), 52.0985),
        (T(1982, 1, 1), 52.1668),
        (T(1982, 2, 1), 52.2316),
        (T(1982, 3, 1), 52.2938),
        (T(1982, 4, 1), 52.3680),
        (T(1982, 5, 1), 52.4465),
        (T(1982, 6, 1), 52.5179),
        (T(1982, 7, 1), 52.5751),
        (T(1982, 8, 1), 52.6178),
        (T(1982, 9, 1), 52.6668),
        (T(1982, 10, 1), 52.7340),
        (T(1982, 11, 1), 52.8056),
        (T(1982, 12, 1), 52.8792),
        (T(1983, 1, 1), 52.9565),
        (T(1983, 2, 1), 53.0445),
        (T(1983, 3, 1), 53.1268),
        (T(1983, 4, 1), 53.2197),
        (T(1983, 5, 1), 53.3024),
        (T(1983, 6, 1), 53.3747),
        (T(1983, 7, 1), 53.4335),
        (T(1983, 8, 1), 53.4778),
        (T(1983, 9, 1), 53.5300),
        (T(1983, 10, 1), 53.5845),
        (T(1983, 11, 1), 53.6523),
        (T(1983, 12, 1), 53.7256),
        (T(1984, 1, 1), 53.7882),
        (T(1984, 2, 1), 53.8367),
        (T(1984, 3, 1), 53.8830),
        (T(1984, 4, 1), 53.9443),
        (T(1984, 5, 1), 54.0042),
        (T(1984, 6, 1), 54.0536),
        (T(1984, 7, 1), 54.0856),
        (T(1984, 8, 1), 54.1084),
        (T(1984, 9, 1), 54.1463),
        (T(1984, 10, 1), 54.1914),
        (T(1984, 11, 1), 54.2452),
        (T(1984, 12, 1), 54.2958),
        (T(1985, 1, 1), 54.3427),
        (T(1985, 2, 1), 54.3911),
        (T(1985, 3, 1), 54.4320),
        (T(1985, 4, 1), 54.4898),
        (T(1985, 5, 1), 54.5456),
        (T(1985, 6, 1), 54.5977),
        (T(1985, 7, 1), 54.6355),
        (T(1985, 8, 1), 54.6532),
        (T(1985, 9, 1), 54.6776),
        (T(1985, 10, 1), 54.7174),
        (T(1985, 11, 1), 54.7741),
        (T(1985, 12, 1), 54.8253),
        (T(1986, 1, 1), 54.8712),
        (T(1986, 2, 1), 54.9161),
        (T(1986, 3, 1), 54.9580),
        (T(1986, 4, 1), 54.9997),
        (T(1986, 5, 1), 55.0476),
        (T(1986, 6, 1), 55.0912),
        (T(1986, 7, 1), 55.1132),
        (T(1986, 8, 1), 55.1328),
        (T(1986, 9, 1), 55.1532),
        (T(1986, 10, 1), 55.1898),
        (T(1986, 11, 1), 55.2415),
        (T(1986, 12, 1), 55.2838),
        (T(1987, 1, 1), 55.3222),
        (T(1987, 2, 1), 55.3613),
        (T(1987, 3, 1), 55.4063),
        (T(1987, 4, 1), 55.4629),
        (T(1987, 5, 1), 55.5111),
        (T(1987, 6, 1), 55.5524),
        (T(1987, 7, 1), 55.5812),
        (T(1987, 8, 1), 55.6004),
        (T(1987, 9, 1), 55.6262),
        (T(1987, 10, 1), 55.6656),
        (T(1987, 11, 1), 55.7168),
        (T(1987, 12, 1), 55.7698),
        (T(1988, 1, 1), 55.8197),
        (T(1988, 2, 1), 55.8615),
        (T(1988, 3, 1), 55.9130),
        (T(1988, 4, 1), 55.9663),
        (T(1988, 5, 1), 56.0220),
        (T(1988, 6, 1), 56.0700),
        (T(1988, 7, 1), 56.0939),
        (T(1988, 8, 1), 56.1105),
        (T(1988, 9, 1), 56.1314),
        (T(1988, 10, 1), 56.1611),
        (T(1988, 11, 1), 56.2068),
        (T(1988, 12, 1), 56.2582),
        (T(1989, 1, 1), 56.3000),
        (T(1989, 2, 1), 56.3399),
        (T(1989, 3, 1), 56.3790),
        (T(1989, 4, 1), 56.4283),
        (T(1989, 5, 1), 56.4804),
        (T(1989, 6, 1), 56.5352),
        (T(1989, 7, 1), 56.5697),
        (T(1989, 8, 1), 56.5983),
        (T(1989, 9, 1), 56.6328),
        (T(1989, 10, 1), 56.6739),
        (T(1989, 11, 1), 56.7332),
        (T(1989, 12, 1), 56.7972),
        (T(1990, 1, 1), 56.8553),
        (T(1990, 2, 1), 56.9111),
        (T(1990, 3, 1), 56.9755),
        (T(1990, 4, 1), 57.0471),
        (T(1990, 5, 1), 57.1136),
        (T(1990, 6, 1), 57.1738),
        (T(1990, 7, 1), 57.2226),
        (T(1990, 8, 1), 57.2597),
        (T(1990, 9, 1), 57.3073),
        (T(1990, 10, 1), 57.3643),
        (T(1990, 11, 1), 57.4334),
        (T(1990, 12, 1), 57.5016),
        (T(1991, 1, 1), 57.5653),
        (T(1991, 2, 1), 57.6333),
        (T(1991, 3, 1), 57.6973),
        (T(1991, 4, 1), 57.7711),
        (T(1991, 5, 1), 57.8407),
        (T(1991, 6, 1), 57.9058),
        (T(1991, 7, 1), 57.9576),
        (T(1991, 8, 1), 57.9975),
        (T(1991, 9, 1), 58.0425),
        (T(1991, 10, 1), 58.1043),
        (T(1991, 11, 1), 58.1679),
        (T(1991, 12, 1), 58.2389),
        (T(1992, 1, 1), 58.3092),
        (T(1992, 2, 1), 58.3833),
        (T(1992, 3, 1), 58.4537),
        (T(1992, 4, 1), 58.5401),
        (T(1992, 5, 1), 58.6228),
        (T(1992, 6, 1), 58.6917),
        (T(1992, 7, 1), 58.7410),
        (T(1992, 8, 1), 58.7836),
        (T(1992, 9, 1), 58.8405),
        (T(1992, 10, 1), 58.8986),
        (T(1992, 11, 1), 58.9714),
        (T(1992, 12, 1), 59.0438),
        (T(1993, 1, 1), 59.1218),
        (T(1993, 2, 1), 59.2003),
        (T(1993, 3, 1), 59.2747),
        (T(1993, 4, 1), 59.3574),
        (T(1993, 5, 1), 59.4434),
        (T(1993, 6, 1), 59.5242),
        (T(1993, 7, 1), 59.5850),
        (T(1993, 8, 1), 59.6343),
        (T(1993, 9, 1), 59.6928),
        (T(1993, 10, 1), 59.7588),
        (T(1993, 11, 1), 59.8386),
        (T(1993, 12, 1), 59.9111),
        (T(1994, 1, 1), 59.9844),
        (T(1994, 2, 1), 60.0564),
        (T(1994, 3, 1), 60.1231),
        (T(1994, 4, 1), 60.2042),
        (T(1994, 5, 1), 60.2804),
        (T(1994, 6, 1), 60.3530),
        (T(1994, 7, 1), 60.4012),
        (T(1994, 8, 1), 60.4440),
        (T(1994, 9, 1), 60.4900),
        (T(1994, 10, 1), 60.5578),
        (T(1994, 11, 1), 60.6324),
        (T(1994, 12, 1), 60.7059),
        (T(1995, 1, 1), 60.7853),
        (T(1995, 2, 1), 60.8663),
        (T(1995, 3, 1), 60.9387),
        (T(1995, 4, 1), 61.0277),
        (T(1995, 5, 1), 61.1103),
        (T(1995, 6, 1), 61.1870),
        (T(1995, 7, 1), 61.2454),
        (T(1995, 8, 1), 61.2881),
        (T(1995, 9, 1), 61.3378),
        (T(1995, 10, 1), 61.4036),
        (T(1995, 11, 1), 61.4760),
        (T(1995, 12, 1), 61.5525),
        (T(1996, 1, 1), 61.6287),
        (T(1996, 2, 1), 61.6846),
        (T(1996, 3, 1), 61.7433),
        (T(1996, 4, 1), 61.8132),
        (T(1996, 5, 1), 61.8823),
        (T(1996, 6, 1), 61.9497),
        (T(1996, 7, 1), 61.9969),
        (T(1996, 8, 1), 62.0343),
        (T(1996, 9, 1), 62.0714),
        (T(1996, 10, 1), 62.1202),
        (T(1996, 11, 1), 62.1809),
        (T(1996, 12, 1), 62.2382),
        (T(1997, 1, 1), 62.2950),
        (T(1997, 2, 1), 62.3506),
        (T(1997, 3, 1), 62.3995),
        (T(1997, 4, 1), 62.4754),
        (T(1997, 5, 1), 62.5463),
        (T(1997, 6, 1), 62.6136),
        (T(1997, 7, 1), 62.6571),
        (T(1997, 8, 1), 62.6942),
        (T(1997, 9, 1), 62.7383),
        (T(1997, 10, 1), 62.7926),
        (T(1997, 11, 1), 62.8567),
        (T(1997, 12, 1), 62.9146),
        (T(1998, 1, 1), 62.9659),
        (T(1998, 2, 1), 63.0217),
        (T(1998, 3, 1), 63.0807),
        (T(1998, 4, 1), 63.1462),
        (T(1998, 5, 1), 63.2053),
        (T(1998, 6, 1), 63.2599),
        (T(1998, 7, 1), 63.2844),
        (T(1998, 8, 1), 63.2961),
        (T(1998, 9, 1), 63.3126),
        (T(1998, 10, 1), 63.3422),
        (T(1998, 11, 1), 63.3871),
        (T(1998, 12, 1), 63.4339),
        (T(1999, 1, 1), 63.4673),
        (T(1999, 2, 1), 63.4979),
        (T(1999, 3, 1), 63.5319),
        (T(1999, 4, 1), 63.5679),
        (T(1999, 5, 1), 63.6104),
        (T(1999, 6, 1), 63.6444),
        (T(1999, 7, 1), 63.6642),
        (T(1999, 8, 1), 63.6739),
        (T(1999, 9, 1), 63.6926),
        (T(1999, 10, 1), 63.7147),
        (T(1999, 11, 1), 63.7518),
        (T(1999, 12, 1), 63.7927),
        (T(2000, 1, 1), 63.8285),
        (T(2000, 2, 1), 63.8557),
        (T(2000, 3, 1), 63.8804),
        (T(2000, 4, 1), 63.9075),
        (T(2000, 5, 1), 63.9393),
        (T(2000, 6, 1), 63.9691),
        (T(2000, 7, 1), 63.9799),
        (T(2000, 8, 1), 63.9833),
        (T(2000, 9, 1), 63.9938),
        (T(2000, 10, 1), 64.0093),
        (T(2000, 11, 1), 64.0400),
        (T(2000, 12, 1), 64.0670),
        (T(2001, 1, 1), 64.0908),
        (T(2001, 2, 1), 64.1068),
        (T(2001, 3, 1), 64.1282),
        (T(2001, 4, 1), 64.1584),
        (T(2001, 5, 1), 64.1833),
        (T(2001, 6, 1), 64.2094),
        (T(2001, 7, 1), 64.2117),
        (T(2001, 8, 1), 64.2073),
        (T(2001, 9, 1), 64.2116),
        (T(2001, 10, 1), 64.2223),
        (T(2001, 11, 1), 64.2500),
        (T(2001, 12, 1), 64.2761),
        (T(2002, 1, 1), 64.2998),
        (T(2002, 2, 1), 64.3192),
        (T(2002, 3, 1), 64.3450),
        (T(2002, 4, 1), 64.3735),
        (T(2002, 5, 1), 64.3943),
        (T(2002, 6, 1), 64.4151),
        (T(2002, 7, 1), 64.4132),
        (T(2002, 8, 1), 64.4118),
        (T(2002, 9, 1), 64.4097),
        (T(2002, 10, 1), 64.4168),
        (T(2002, 11, 1), 64.4329),
        (T(2002, 12, 1), 64.4511),
        (T(2003, 1, 1), 64.4734),
        (T(2003, 2, 1), 64.4893),
        (T(2003, 3, 1), 64.5053),
        (T(2003, 4, 1), 64.5269),
        (T(2003, 5, 1), 64.5471),
        (T(2003, 6, 1), 64.5597),
        (T(2003, 7, 1), 64.5512),
        (T(2003, 8, 1), 64.5371),
        (T(2003, 9, 1), 64.5359),
        (T(2003, 10, 1), 64.5415),
        (T(2003, 11, 1), 64.5544),
        (T(2003, 12, 1), 64.5654),
        (T(2004, 1, 1), 64.5736),
        (T(2004, 2, 1), 64.5891),
        (T(2004, 3, 1), 64.6015),
        (T(2004, 4, 1), 64.6176),
        (T(2004, 5, 1), 64.6374),
        (T(2004, 6, 1), 64.6549),
        (T(2004, 7, 1), 64.6530),
        (T(2004, 8, 1), 64.6379),
        (T(2004, 9, 1), 64.6372),
        (T(2004, 10, 1), 64.6400),
        (T(2004, 11, 1), 64.6543),
        (T(2004, 12, 1), 64.6723),
        (T(2005, 1, 1), 64.6876),
        (T(2005, 2, 1), 64.7052),
        (T(2005, 3, 1), 64.7313),
        (T(2005, 4, 1), 64.7575),
        (T(2005, 5, 1), 64.7811),
        (T(2005, 6, 1), 64.8001),
        (T(2005, 7, 1), 64.7995),
        (T(2005, 8, 1), 64.7876),
        (T(2005, 9, 1), 64.7831),
        (T(2005, 10, 1), 64.7921),
        (T(2005, 11, 1), 64.8096),
        (T(2005, 12, 1), 64.8311),
        (T(2006, 1, 1), 64.8452),
        (T(2006, 2, 1), 64.8597),
        (T(2006, 3, 1), 64.8850),
        (T(2006, 4, 1), 64.9175),
        (T(2006, 5, 1), 64.9480),
        (T(2006, 6, 1), 64.9794),
        (T(2006, 7, 1), 64.9895),
        (T(2006, 8, 1), 65.0028),
        (T(2006, 9, 1), 65.0138),
        (T(2006, 10, 1), 65.0371),
        (T(2006, 11, 1), 65.0773),
        (T(2006, 12, 1), 65.1122),
        (T(2007, 1, 1), 65.1464),
        (T(2007, 2, 1), 65.1833),
        (T(2007, 3, 1), 65.2145),
        (T(2007, 4, 1), 65.2494),
        (T(2007, 5, 1), 65.2921),
        (T(2007, 6, 1), 65.3279),
        (T(2007, 7, 1), 65.3413),
        (T(2007, 8, 1), 65.3452),
        (T(2007, 9, 1), 65.3496),
        (T(2007, 10, 1), 65.3711),
        (T(2007, 11, 1), 65.3972),
        (T(2007, 12, 1), 65.4296),
        (T(2008, 1, 1), 65.4574),
        (T(2008, 2, 1), 65.4868),
        (T(2008, 3, 1), 65.5152),
        (T(2008, 4, 1), 65.5450),
        (T(2008, 5, 1), 65.5781),
        (T(2008, 6, 1), 65.6127),
        (T(2008, 7, 1), 65.6288),
        (T(2008, 8, 1), 65.6370),
        (T(2008, 9, 1), 65.6493),
        (T(2008, 10, 1), 65.6760),
        (T(2008, 11, 1), 65.7097),
        (T(2008, 12, 1), 65.7461),
        (T(2009, 1, 1), 65.7768),
        (T(2009, 2, 1), 65.8025),
        (T(2009, 3, 1), 65.8237),
        (T(2009, 4, 1), 65.8595),
        (T(2009, 5, 1), 65.8973),
        (T(2009, 6, 1), 65.9323),
        (T(2009, 7, 1), 65.9509),
        (T(2009, 8, 1), 65.9534),
        (T(2009, 9, 1), 65.9628),
        (T(2009, 10, 1), 65.9839),
        (T(2009, 11, 1), 66.0147),
        (T(2009, 12, 1), 66.0421),
        (T(2010, 1, 1), 66.0699),
        (T(2010, 2, 1), 66.0961),
        (T(2010, 3, 1), 66.1310),
        (T(2010, 4, 1), 66.1683),
        (T(2010, 5, 1), 66.2072),
        (T(2010, 6, 1), 66.2356),
        (T(2010, 7, 1), 66.2409),
        (T(2010, 8, 1), 66.2335),
        (T(2010, 9, 1), 66.2349),
        (T(2010, 10, 1), 66.2441),
        (T(2010, 11, 1), 66.2751),
        (T(2010, 12, 1), 66.3054),
        (T(2011, 1, 1), 66.3246),
        (T(2011, 2, 1), 66.3406),
        (T(2011, 3, 1), 66.3624),
        (T(2011, 4, 1), 66.3957),
        (T(2011, 5, 1), 66.4289),
        (T(2011, 6, 1), 66.4619),
        (T(2011, 7, 1), 66.4749),
        (T(2011, 8, 1), 66.4751),
        (T(2011, 9, 1), 66.4829),
        (T(2011, 10, 1), 66.5056),
        (T(2011, 11, 1), 66.5383),
        (T(2011, 12, 1), 66.5706),
        (T(2012, 1, 1), 66.6030),
        (T(2012, 2, 1), 66.6340),
        (T(2012, 3, 1), 66.6569),
        (T(2012, 4, 1), 66.6925),
        (T(2012, 5, 1), 66.7289),
        (T(2012, 6, 1), 66.7579),
        (T(2012, 7, 1), 66.7708),
        (T(2012, 8, 1), 66.7740),
        (T(2012, 9, 1), 66.7846),
        (T(2012, 10, 1), 66.8103),
        (T(2012, 11, 1), 66.8401),
        (T(2012, 12, 1), 66.8779),
        (T(2013, 1, 1), 66.9069),
        (T(2013, 2, 1), 66.9443),
        (T(2013, 3, 1), 66.9763),
        (T(2013, 4, 1), 67.0258),
        (T(2013, 5, 1), 67.0716),
        (T(2013, 6, 1), 67.1100),
        (T(2013, 7, 1), 67.1266),
        (T(2013, 8, 1), 67.1331),
        (T(2013, 9, 1), 67.1458),
        (T(2013, 10, 1), 67.1717),
        (T(2013, 11, 1), 67.2091),
        (T(2013, 12, 1), 67.2460),
        (T(2014, 1, 1), 67.2810),
        (T(2014, 2, 1), 67.3136),
        (T(2014, 3, 1), 67.3457),
        (T(2014, 4, 1), 67.3890),
        (T(2014, 5, 1), 67.4318),
        (T(2014, 6, 1), 67.4666),
        (T(2014, 7, 1), 67.4859),
        (T(2014, 8, 1), 67.4989),
        (T(2014, 9, 1), 67.5111),
        (T(2014, 10, 1), 67.5353),
        (T(2014, 11, 1), 67.5711),
        (T(2014, 12, 1), 67.6070),
        (T(2015, 1, 1), 67.6439),
        (T(2015, 2, 1), 67.6765),
        (T(2015, 3, 1), 67.7117),
        (T(2015, 4, 1), 67.7591),
        (T(2015, 5, 1), 67.8012),
        (T(2015, 6, 1), 67.8402),
        (T(2015, 7, 1), 67.8606),
        (T(2015, 8, 1), 67.8822),
        (T(2015, 9, 1), 67.9120),
        (T(2015, 10, 1), 67.9547),
        (T(2015, 11, 1), 68.0055),
        (T(2015, 12, 1), 68.0514),
        (T(2016, 1, 1), 68.1024),
        (T(2016, 2, 1), 68.1577),
        (T(2016, 3, 1), 68.2044),
        (T(2016, 4, 1), 68.2665),
        (T(2016, 5, 1), 68.3188),
        (T(2016, 6, 1), 68.3704),
        (T(2016, 7, 1), 68.3964),
        (T(2016, 8, 1), 68.4095),
        (T(2016, 9, 1), 68.4305),
        (T(2016, 10, 1), 68.4630),
        (T(2016, 11, 1), 68.5078),
        (T(2016, 12, 1), 68.5537),
        (T(2017, 1, 1), 68.5927),
        (T(2017, 2, 1), 68.6298),
        (T(2017, 3, 1), 68.6671),
        (T(2017, 4, 1), 68.7135),
        (T(2017, 5, 1), 68.7623),
        (T(2017, 6, 1), 68.8033),
        (T(2017, 7, 1), 68.8245),
        (T(2017, 8, 1), 68.8373),
        (T(2017, 9, 1), 68.8477),
        (T(2017, 10, 1), 68.8689),
        (T(2017, 11, 1), 68.9006),
        (T(2017, 12, 1), 68.9355),
        (T(2018, 1, 1), 68.9677),
        (T(2018, 3, 1), 69.14),
        (T(2018, 6, 1), 69.3),
        (T(2018, 9, 1), 69.3),
        (T(2019, 1, 1), 69.5),
        (T(2019, 3, 1), 69.6),
        (T(2019, 6, 1), 69.7),
        (T(2019, 9, 1), 69.8),
        (T(2020, 1, 1), 69.9),
        (T(2020, 3, 1), 70.0),
        (T(2020, 6, 1), 70.0),
        (T(2020, 9, 1), 70.0),
        (T(2021, 1, 1), 70.0),
        (T(2021, 3, 1), 70.0),
        (T(2021, 6, 1), 70.0),
        (T(2021, 9, 1), 70.0),
        (T(2022, 1, 1), 70.0),
        (T(2022, 3, 1), 70.0),
        (T(2022, 6, 1), 71.0),
        (T(2022, 9, 1), 71.0),
        (T(2023, 1, 1), 71.0),
        (T(2023, 3, 1), 71.0),
        (T(2023, 6, 1), 71.0),
        (T(2023, 9, 1), 71.0),
        (T(2024, 1, 1), 71.0),
        (T(2024, 3, 1), 71.0),
        (T(2024, 6, 1), 71.0),
        (T(2024, 9, 1), 71.0),
        (T(2025, 1, 1), 71.0),
        (T(2025, 3, 1), 72.0),
        (T(2025, 6, 1), 72.0),
        (T(2025, 9, 1), 72.0),
        (T(2026, 1, 1), 72.0),
    ],
    dtype=[("time", "M8[D]"), ("julian_days", "f8")],
)
# seconds in a julian day
utc_to_jd_tt.corrections["julian_days"] /= 60 * 60 * 24


def compute_ecliptic_longitude(ob, minutes):
    """Compute an objects ecliptic longitude..

    Parameters
    ----------
    minutes : np.array[M8[m]]
        The minutes to compute the longitude for.

    Returns
    -------
    rad : np.ndarray[float64]
        The ecliptic longitude of the object (in radians).

    Notes
    -----
    The ``novas`` ephemeris must be opened before this can be called.
    """
    longitude_degrees = np.array(
        [
            novas.equ2ecl(
                jd_tt,
                # right ascension and declination
                *novas.app_planet(jd_tt, ob)[:2],
                # true equator and equinox of date
                coord_sys=1,
            )[0]
            for jd_tt in utc_to_jd_tt(minutes)
        ]
    )
    return np.deg2rad(longitude_degrees)


def lunar_ecliptic_longitude_block(root, minutes):
    """Calculate and save one block of moon phase data.

    Parameters
    ----------
    root : pathlib.Path
        The directory to save the data to.
    minutes : np.array[M8[m]]
        The minutes to compute the moon phase for.
    """
    ob = novas.make_object(0, 11, "Moon", None)
    el = compute_ecliptic_longitude(ob, minutes)
    path = str(root / f"{minutes[0]}_{minutes[-1]}")
    el.tofile(str(path))


def print_dates(dates, *, weekday):
    """Print an array of dates.

    Parameters
    ----------
    dates : np.ndarray[M8]
        The dates to print.
    weekday : bool
        Print the day of the week in a second column?
    """
    dates = dates.astype("M8[D]")

    if weekday:
        weekdays = pd.to_datetime(dates).weekday_name

        for d, weekday in zip(dates, weekdays):
            print(d, weekday)
    else:
        for d in dates:
            print(d)


@click.group()
def main():
    """Utilities for computing the Gregorian dates of lunisolar calendar
    events.
    """


@main.command("lunar-ecliptic-longitude")
@click.option(
    "--root-dir",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to save the data to.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--verbose/--quiet",
    default=False,
)
@click.option(
    "--start",
    help="The start date to compute.",
    default="1980-01-01",
)
@click.option(
    "--stop",
    help="The stop date to compute.",
    default="2050-01-01",
)
def lunar_ecliptic_longitude(root_dir, verbose, start, stop):
    """Simulate the ecliptic longitude of the moon."""
    root_dir = pathlib.Path(root_dir)
    root_dir.mkdir(parents=True, exist_ok=True)

    with ephemeris():
        minutes = np.arange(start, stop, dtype="M8[m]")
        for minutes in toolz.partition_all(60 * 24 * 365, minutes):
            if verbose:
                print(f"start={minutes[0]}; stop={minutes[-1]}")

            lunar_ecliptic_longitude_block(root_dir, np.array(minutes))


def calculate_new_moon(solar_ecliptic_longitude, lunar_ecliptic_longitude, minutes):
    """Calculate when the new moon will occur.

    Parameters
    ----------
    solar_ecliptic_longitude : np.ndarray[f8]
        The ecliptic longitude of the sun.
    lunar_ecliptic_longitude : np.ndarray[f8]
        The ecliptic longitude of the moon.
    minutes : np.ndarray[M8[m]]
        The minute labels for the longitude data.

    Returns
    -------
    new_moons : np.ndarray[M8[m]]
        The minutes when a new moon occurs.
    """
    diff = np.abs(solar_ecliptic_longitude - lunar_ecliptic_longitude)
    new_moon_where = local_cmp(op.lt, diff)
    # clean up some local mins where the value isn't close to 0
    new_moon_where = new_moon_where[diff[new_moon_where] < 0.1]
    return minutes[new_moon_where]


def load_new_moons(
    solar_ecliptic_longitude, lunar_ecliptic_longitude, *, return_solar_data=False
):
    """Load the new moon minutes from the precomputed data.

    Parameters
    ----------
    solar_ecliptic_longitude : path-like
        The root dir to read solar ecliptic longitude data from.
    lunar_ecliptic_longitude : path-like
        The root dir to read lunar ecliptic longitude data from.
    return_solar_data : bool, optional
        Also return the solar data.

    Returns
    -------
    new_moons : np.ndarray[M8[m]]
        The minutes of the new moons.
    """
    solar_elon, solar_minutes = load_precomputed_data(solar_ecliptic_longitude)
    lunar_elon, lunar_minutes = load_precomputed_data(lunar_ecliptic_longitude)

    if not np.all(solar_minutes == lunar_minutes):
        raise ValueError(
            "mismatched minutes for solar and lunar ecliptic longitude",
        )

    new_moons = calculate_new_moon(
        solar_elon,
        lunar_elon,
        # the minutes are equal so it doesn't matter which we use
        solar_minutes,
    )
    if return_solar_data:
        return new_moons, solar_elon, solar_minutes

    return new_moons


@main.command("new-moon")
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--lunar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read lunar ecliptic longitude data from.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "-o",
    "--output",
    type=click.File("w"),
    default=None,
    help="The output to write the array of the minutes for each new moon.",
)
def new_moon(solar_ecliptic_longitude, lunar_ecliptic_longitude, output):
    """Calculate the minute of each new moon."""
    new_moon_minutes = load_new_moons(
        solar_ecliptic_longitude,
        lunar_ecliptic_longitude,
    )
    if output is None:
        print(new_moon_minutes)
    else:
        new_moon_minutes.tofile(output)


def solar_ecliptic_longitude_block(root, minutes):
    """Compute and save the sun's ecliptic longitude.

    Parameters
    ----------
    minutes : np.array[M8[m]]
        The minutes to compute the sun's position.
    """
    ob = novas.make_object(0, 10, "Sun", None)
    el = compute_ecliptic_longitude(ob, minutes)
    path = str(root / f"{minutes[0]}_{minutes[-1]}")
    el.tofile(str(path))


@main.command("solar-ecliptic-longitude")
@click.option(
    "--root-dir",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to save the data to.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--verbose/--quiet",
    default=False,
)
@click.option(
    "--start",
    help="The start date to compute.",
    default="1980-01-01",
)
@click.option(
    "--stop",
    help="The stop date to compute.",
    default="2050-01-01",
)
def solar_solar_ecliptic_longitude(root_dir, verbose, start, stop):
    """Simulate the ecliptic longitude of the sun."""
    root_dir = pathlib.Path(root_dir)
    root_dir.mkdir(parents=True, exist_ok=True)

    with ephemeris():
        minutes = np.arange(start, stop, dtype="M8[m]")
        for minutes in toolz.partition_all(60 * 24 * 365, minutes):
            if verbose:
                print(f"start={minutes[0]}; stop={minutes[-1]}")

            solar_ecliptic_longitude_block(root_dir, np.array(minutes))


def minutes_at_phase(longitude, minutes, phase):
    """Find the minutes where the longitude is nearest to ``phase``.

    Parameters
    ----------
    longitude : np.ndarray[float64]
        The longitude (in radians) of the sun.
    minutes : np.ndarray[M8[m]]
        The minute labels for the longitude values.
    phase : float
        The target longitude to find the minutes for.

    Returns
    -------
    minutes_at_phase : np.ndarray[M8[m]]
        The minutes where ``longitude`` is closest to ``phase``.
    """
    return minutes[local_cmp(op.lt, np.abs(longitude - phase))]


def calculate_new_year(solar_longitude, minutes_for_longitude, new_moons):
    """Calculate the Chinese new year new moon times.

    Parameters
    ----------
    solar_ecliptic_longitude_dir : path-like
        The root dir to read solar ecliptic longitude data from.
    new_moons : np.ndarray[M8[m]]
        The sorted minutes when the new moon occurs.

    Returns
    -------
    new_years : np.ndarray[M8[m]]
        The minutes when the first new moon of each year occurs.
    """
    months, eleventh_month_ix = chinese_month_start(
        new_moons,
        solar_longitude,
        minutes_for_longitude,
        return_11th_month=True,
    )
    new_year_ix = eleventh_month_ix + 2
    return months[new_year_ix[new_year_ix < len(months)]]


def utc_to_chinese_date(arr):
    """Convert UTC minutes to a chinese date.

    Parameters
    ----------
    arr : np.array[M8[m]]
        The datetimes to convert.

    Returns
    -------
    dates : np.array[M8[ns]]
        An array of datetime64[ns] at midnight corresponding the date in China
        of the input.
    """
    ts = pd.to_datetime(arr).tz_localize(UTC).tz_convert("Asia/Shanghai")
    out = ts.normalize().tz_localize(None)
    if isinstance(out, pd.Timestamp):
        return out.asm8
    return out.values


@main.command("chinese-new-year")
@click.option(
    "--lunar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read lunar ecliptic longitude data from.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "-o",
    "--output",
    type=click.File("w"),
    default=None,
    help="The output to write the dates of the Chinese New Year.",
)
@click.option(
    "--weekday/--no-weekday",
    default=False,
    help="If printing to stdout, include the weekday?",
)
def chinese_new_year(
    lunar_ecliptic_longitude, solar_ecliptic_longitude, output, weekday
):
    """Calculate the Gregorian date of Chinese New Year."""
    new_moons, solar_elon, solar_minutes = load_new_moons(
        solar_ecliptic_longitude,
        lunar_ecliptic_longitude,
        return_solar_data=True,
    )

    chinese_new_year = calculate_new_year(
        solar_elon,
        solar_minutes,
        new_moons,
    )

    chinese_new_year = utc_to_chinese_date(chinese_new_year)
    if output is None:
        print_dates(chinese_new_year, weekday=weekday)
    else:
        chinese_new_year.tofile(output)


@main.command("qingming-festival")
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "-o",
    "--output",
    type=click.File("w"),
    default=None,
    help="The output to write the dates of the Qingming Festival.",
)
@click.option(
    "--weekday/--no-weekday",
    default=False,
    help="If printing to stdout, include the weekday?",
)
def qingming_festival(solar_ecliptic_longitude, output, weekday):
    """Calculate the Gregorian date of Qingming Festival."""
    longitude, minutes = load_precomputed_data(solar_ecliptic_longitude)

    # The Qingming (清明) festival is celebrated on the calendar day of the
    # first minor solar period (节气). A solar period is defined as the sun
    # moving 1/24th of the ecliptic, with 0 being the vernal equinox (春分),
    # and 1/2 of a rotation being the autumnal equinox (秋分).
    qingming_minute = minutes_at_phase(longitude, minutes, np.pi / 12)

    qingming_festival_date = utc_to_chinese_date(qingming_minute)
    if output is None:
        print_dates(qingming_festival_date, weekday=weekday)
    else:
        qingming_festival_date.tofile(output)


def chinese_month_start(
    new_moons, solar_longitude, minutes, *, return_11th_month=False
):
    """Get the start date of each lunar month (excluding leap months).

    Parameters
    ----------
    new_moons : np.ndarray[M8[m]]
        The minute of each new moon.
    solar_longitude : np.ndarray[f8]
        An array of the ecliptical longitude of the sun.
    minutes : np.ndarray[M8[m]]
        The labels for ``solar_longitude``.
    return_11th_month : bool, optional
        Return the index of the 11th month.

    Returns
    -------
    month_starts : np.ndarray[M8[m]]
        The minute of the new moon that starts each real month.
    """
    winter_solstice = minutes_at_phase(
        solar_longitude,
        minutes,
        3 * np.pi / 2,
    )
    sui_start_index = (
        utc_to_chinese_date(new_moons)
        > utc_to_chinese_date(winter_solstice).reshape(-1, 1)
    ).argmax(axis=1) - 1

    get_next_sui_ix = iter(sui_start_index).__next__

    next_sui_ix = get_next_sui_ix()
    next_sui = new_moons[next_sui_ix]
    prev_sui_ix = next_sui_ix - 13

    seen_skipped_month = False
    month_starts = []

    zhongqi = minutes_at_phase(
        np.fmod(solar_longitude, np.pi / 6),
        minutes,
        0,
    )[1:]
    contains_zhongqi_ix = (
        utc_to_chinese_date(zhongqi).reshape(-1, 1) < utc_to_chinese_date(new_moons)
    ).argmax(axis=1) - 1
    months_with_zhongqi = np.unique(new_moons[contains_zhongqi_ix])
    possible_skipped_months = np.setdiff1d(new_moons, months_with_zhongqi)

    for new_moon in new_moons:
        if new_moon == next_sui:
            seen_skipped_month = False

        possible_skipped_month = new_moon in possible_skipped_months
        skip = (
            possible_skipped_month
            and not seen_skipped_month
            # check the number of new moons between the two sui to guard
            # against fake leap months (e.g. 8th month of 2033)
            and next_sui_ix - prev_sui_ix != 12
        )

        if new_moon == next_sui:
            prev_sui_ix = next_sui_ix
            try:
                next_sui_ix = get_next_sui_ix()
                next_sui = new_moons[next_sui_ix]
            except StopIteration:
                pass

        if skip:
            # we only skip the first month without a zhongqi in a given year
            seen_skipped_month = True
        else:
            month_starts.append(new_moon)

    month_starts = np.array(month_starts)
    if return_11th_month:
        return (month_starts, np.searchsorted(month_starts, new_moons[sui_start_index]))
    return month_starts


def nth_day_of_nth_chinese_month(
    lunar_ecliptic_longitude, solar_ecliptic_longitude, month, day
):
    """Helper for computing the nth day of the nth lunar month from the cached
    data.

    Parameters
    ----------
    lunar_ecliptic_longitude : path-like
        The root dir to read lunar ecliptic longitude data from.
    solar_ecliptic_longitude : path-like
        The root dir to read solar ecliptic longitude data from.
    month : int
        The one indexed month number.
    day : int
        The one indexed day number.

    Returns
    -------
    dates : np.ndarray[M8[D]]
        The date of the holiday each year.
    """
    new_moons, solar_elon, solar_minutes = load_new_moons(
        solar_ecliptic_longitude,
        lunar_ecliptic_longitude,
        return_solar_data=True,
    )

    months = chinese_month_start(new_moons, solar_elon, solar_minutes)
    chinese_new_year = calculate_new_year(
        solar_elon,
        solar_minutes,
        new_moons,
    )
    new_year_month_ix = np.searchsorted(months, chinese_new_year)

    # subtract one from month because it is 1-indexed
    month_start = utc_to_chinese_date(months[new_year_month_ix + (month - 1)])
    # subtract one from day because it is 1-indexed.
    return month_start + np.timedelta64(day - 1, "D")


@main.command("china-buddhas-birthday")
@click.option(
    "--lunar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read lunar ecliptic longitude data from.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "-o",
    "--output",
    type=click.File("w"),
    default=None,
    help="The output to write the dates of Buddha's Birthday.",
)
@click.option(
    "--weekday/--no-weekday",
    default=False,
    help="If printing to stdout, include the weekday?",
)
def china_buddhas_birthday(
    lunar_ecliptic_longitude, solar_ecliptic_longitude, output, weekday
):
    """Calculate the Gregorian date of China's observance of Buddha's Birthday."""
    # Buddhas birthday is the 8th day (one indexed) of the 4th chinese lunar
    # month (not counting leap months).
    buddhas_birthday = nth_day_of_nth_chinese_month(
        lunar_ecliptic_longitude,
        solar_ecliptic_longitude,
        month=4,
        day=8,
    )
    if output is None:
        print_dates(buddhas_birthday, weekday=weekday)
    else:
        buddhas_birthday.tofile(output)


@main.command("dragon-boat-festival")
@click.option(
    "--lunar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read lunar ecliptic longitude data from.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "-o",
    "--output",
    type=click.File("w"),
    default=None,
    help="The output to write the dates of the Dragon Boat Festival.",
)
@click.option(
    "--weekday/--no-weekday",
    default=False,
    help="If printing to stdout, include the weekday?",
)
def dragon_boat_festival(
    lunar_ecliptic_longitude, solar_ecliptic_longitude, output, weekday
):
    """Calculate the Gregorian date of the Dragon Boat Festival."""
    # The Dragon Boat Festival is the 5th day of the 5th month.
    # month (not counting leap months).
    dragon_boat_festival = nth_day_of_nth_chinese_month(
        lunar_ecliptic_longitude,
        solar_ecliptic_longitude,
        month=5,
        day=5,
    )
    if output is None:
        print_dates(dragon_boat_festival, weekday=weekday)
    else:
        dragon_boat_festival.tofile(output)


@main.command("mid-autumn-festival")
@click.option(
    "--lunar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read lunar ecliptic longitude data from.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "-o",
    "--output",
    type=click.File("w"),
    default=None,
    help="The output to write the dates of the Mid Autumn Festival.",
)
@click.option(
    "--weekday/--no-weekday",
    default=False,
    help="If printing to stdout, include the weekday?",
)
def mid_autumn_festival(
    lunar_ecliptic_longitude, solar_ecliptic_longitude, output, weekday
):
    """Calculate the Gregorian date of the Mid Autumn Festival."""
    # The Mid Autumn Festival is the 15th day of the 8th month.
    # month (not counting leap months).
    mid_autumn_festival = nth_day_of_nth_chinese_month(
        lunar_ecliptic_longitude,
        solar_ecliptic_longitude,
        month=8,
        day=15,
    )
    if output is None:
        print_dates(mid_autumn_festival, weekday=weekday)
    else:
        mid_autumn_festival.tofile(output)


@main.command("double-ninth-festival")
@click.option(
    "--lunar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read lunar ecliptic longitude data from.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "-o",
    "--output",
    type=click.File("w"),
    default=None,
    help="The output to write the dates of the Double Ninth Festival.",
)
@click.option(
    "--weekday/--no-weekday",
    default=False,
    help="If printing to stdout, include the weekday?",
)
def double_ninth_festival(
    lunar_ecliptic_longitude, solar_ecliptic_longitude, output, weekday
):
    """Calculate the Gregorian date of the Double Ninth Festival."""
    # The Double Ninth Festival is the 9th day of the 9th month.
    # month (not counting leap months).
    double_ninth_festival = nth_day_of_nth_chinese_month(
        lunar_ecliptic_longitude,
        solar_ecliptic_longitude,
        month=9,
        day=9,
    )
    if output is None:
        print_dates(double_ninth_festival, weekday=weekday)
    else:
        double_ninth_festival.tofile(output)


def _render_month(year, months, month, *, print_year):
    import io

    out = io.StringIO()

    # subtract one from month because it is 1-indexed
    month_start = utc_to_chinese_date(months[month - 1])
    month_end = utc_to_chinese_date(months[month])
    days = pd.date_range(month_start, month_end, closed="left")

    is_leap_month = len(days) > 30
    title = f'Lunar Month {month}{"(+)" if is_leap_month else ""}'
    if print_year:
        title += f" {year}"
    print(f"{title:^20}".rstrip(), file=out)
    gregorian_months = f"{days[0].month_name()}-{days[-1].month_name()}"
    print(f"{gregorian_months:^20}".rstrip(), file=out)
    print("Su Mo Tu We Th Fr Sa", file=out)
    print(" " * (3 * ((days[0].weekday() + 1) % 7) - 1), end="", file=out)

    for d in days:
        if d.weekday() == 6:
            print("", file=out)
        else:
            print(" ", end="", file=out)

        print(f"{d.day:>2}", end="", file=out)

    print("", file=out)
    return out.getvalue()


def _concat_lines(strings, width):
    as_lines = [string.splitlines() for string in strings]
    max_lines = max(len(lines) for lines in as_lines)
    for lines in as_lines:
        missing_lines = max_lines - len(lines)
        if missing_lines:
            lines.extend([" " * width] * missing_lines)

    rows = []
    for row_parts in zip(*as_lines):
        row_parts = list(row_parts)
        for n, row_part in enumerate(row_parts):
            missing_space = width - len(row_part)
            if missing_space:
                row_parts[n] = row_part + " " * missing_space

        rows.append("   ".join(row_parts))

    return "\n".join(row.rstrip() for row in rows)


@main.command("chinese-cal")
@click.argument("month", type=int)
@click.argument("year", type=int, required=False)
@click.option(
    "--lunar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read lunar ecliptic longitude data from.",
    default="lunar-ecliptic-longitude",
    show_default=True,
)
@click.option(
    "--solar-ecliptic-longitude",
    type=click.Path(dir_okay=True, file_okay=False),
    help="The root dir to read solar ecliptic longitude data from.",
    default="solar-ecliptic-longitude",
    show_default=True,
)
def chinese_cal(month, year, lunar_ecliptic_longitude, solar_ecliptic_longitude):
    """Print a unix cal like table for a Chinese lunar month. The day numbers
    are the Gregorian days of their Gregorian month.
    """
    if year is None:
        year = month
        month = None
        full_year = True
    else:
        full_year = False

    new_moons, solar_elon, solar_minutes = load_new_moons(
        solar_ecliptic_longitude,
        lunar_ecliptic_longitude,
        return_solar_data=True,
    )

    months = chinese_month_start(new_moons, solar_elon, solar_minutes)
    chinese_new_year = calculate_new_year(
        solar_elon,
        solar_minutes,
        new_moons,
    )
    given_new_year = chinese_new_year[
        chinese_new_year.astype("M8[Y]") == np.datetime64(year - 1970, "Y")
    ]
    if len(given_new_year) == 0:
        raise ValueError(f"data does not contain {year}")

    new_year_month_ix = np.searchsorted(months, given_new_year[0])
    year_months = months[new_year_month_ix:]

    if not full_year:
        print(_render_month(year, year_months, month, print_year=True))
    else:
        month_strings = [
            [
                _render_month(
                    year,
                    year_months,
                    row * 3 + column + 1,
                    print_year=False,
                )
                for column in range(3)
            ]
            for row in range(4)
        ]
        print(f"{year:^66}\n".rstrip())
        print("\n\n".join(_concat_lines(cs, 20) for cs in month_strings))


if __name__ == "__main__":
    main()
