use serde::{Deserialize, Serialize};
use crate::constants::TAU;
use crate::ephemeris::observe::{apparent, ecliptic_latlon, observe};
use crate::ephemeris::KernelSet;
use crate::error::EngineError;
use crate::frames::nutation::iau2000b_radians;
use crate::frames::{mxv, Orientation};
use crate::instant::UtcInstant;
use crate::planets::require_kernel;
use crate::time::Time;
const SUN: i32 = 10;
const MOON: i32 = 301;
const TROPICAL_YEAR: f64 = 365.242_19;
const SYNODIC_MONTH: f64 = 29.530_589;
const SUN_RATE: f64 = 360.0 / TROPICAL_YEAR;
const ELONGATION_RATE: f64 = 360.0 / SYNODIC_MONTH;
const EPSILON_DAYS: f64 = 1e-7;
const UNIX_EPOCH_JD: f64 = 2_440_587.5;
const UNIX_EPOCH_JDN: i64 = 2_440_588;
const MICROS_PER_DAY: i64 = 86_400_000_000;
const DATE_MARGIN: i64 = 3;
const TERM_NAMES: [&str; 24] = [
"chunfen",
"qingming",
"guyu",
"lixia",
"xiaoman",
"mangzhong",
"xiazhi",
"xiaoshu",
"dashu",
"liqiu",
"chushu",
"bailu",
"qiufen",
"hanlu",
"shuangjiang",
"lidong",
"xiaoxue",
"daxue",
"dongzhi",
"xiaohan",
"dahan",
"lichun",
"yushui",
"jingzhe",
];
pub(crate) const LICHUN: u16 = 315;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SolarTerm {
pub name: String,
pub longitude: u16,
pub jie: bool,
pub utc: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LunarMonth {
pub year: i32,
pub month: u8,
pub leap: bool,
pub start: String,
pub days: u8,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChineseCalendar {
pub equation_of_time: f64,
pub solar_terms: Vec<SolarTerm>,
pub lunar_months: Vec<LunarMonth>,
}
impl SolarTerm {
pub(crate) fn instant(&self) -> Result<UtcInstant, EngineError> {
UtcInstant::parse(&self.utc).map_err(|e| EngineError::InvalidInput(e.to_string()))
}
}
impl LunarMonth {
pub(crate) fn start_day(&self) -> Result<i64, EngineError> {
let start = UtcInstant::parse(&format!("{}T00:00:00Z", self.start))
.map_err(|e| EngineError::InvalidInput(e.to_string()))?;
Ok(day_number(start))
}
}
pub(crate) fn day_number(instant: UtcInstant) -> i64 {
instant.micros().div_euclid(MICROS_PER_DAY) + UNIX_EPOCH_JDN
}
fn date_of(day: i64) -> String {
let (y, m, d, ..) = UtcInstant::from_micros((day - UNIX_EPOCH_JDN) * MICROS_PER_DAY).civil();
format!("{y:04}-{m:02}-{d:02}")
}
fn instant_of(jd: f64) -> UtcInstant {
let seconds = ((jd - UNIX_EPOCH_JD) * 86_400.0).round() as i64;
UtcInstant::from_micros(seconds * 1_000_000)
}
fn wrap180(degrees: f64) -> f64 {
(degrees + 180.0).rem_euclid(360.0) - 180.0
}
fn china_offset(jd: f64) -> f64 {
const CST_SINCE_JD: f64 = 2_425_612.5 - 8.0 / 24.0;
if jd < CST_SINCE_JD {
(116.0 + 25.0 / 60.0) / 360.0
} else {
8.0 / 24.0
}
}
fn china_day(jd: f64) -> i64 {
(jd + 0.5 + china_offset(jd)).floor() as i64
}
struct Sky<'a> {
kernels: &'a KernelSet,
}
impl Sky<'_> {
fn longitude(&self, target: i32, jd: f64) -> Result<f64, EngineError> {
let kernel = require_kernel(self.kernels, jd)?;
let t = Time::from_ut1(jd);
let orientation = Orientation::with_nutation(&t, iau2000b_radians(t.tt()));
let position = apparent(kernel, &observe(kernel, target, &t)?, &t)?;
Ok(ecliptic_latlon(&orientation, &position).1)
}
fn sun(&self, jd: f64) -> Result<f64, EngineError> {
self.longitude(SUN, jd)
}
fn elongation(&self, jd: f64) -> Result<f64, EngineError> {
Ok(self.longitude(MOON, jd)? - self.sun(jd)?)
}
fn sun_at(&self, longitude: f64, guess: f64) -> Result<f64, EngineError> {
crossing(|jd| self.sun(jd), longitude, guess, SUN_RATE)
}
fn new_moon_near(&self, guess: f64) -> Result<f64, EngineError> {
crossing(|jd| self.elongation(jd), 0.0, guess, ELONGATION_RATE)
}
fn sun_before(&self, longitude: f64, jd: f64) -> Result<f64, EngineError> {
let behind = (self.sun(jd)? - longitude).rem_euclid(360.0);
let found = self.sun_at(longitude, jd - behind / SUN_RATE)?;
if found > jd {
self.sun_at(longitude, found - TROPICAL_YEAR)
} else {
Ok(found)
}
}
fn new_moon_before(&self, jd: f64) -> Result<f64, EngineError> {
let behind = self.elongation(jd)?.rem_euclid(360.0);
let found = self.new_moon_near(jd - behind / ELONGATION_RATE)?;
if found > jd {
self.new_moon_near(found - SYNODIC_MONTH)
} else {
Ok(found)
}
}
fn month_eleven(&self, solstice: f64) -> Result<f64, EngineError> {
let solstice_day = china_day(solstice);
let moon = self.new_moon_before(solstice + 1.0)?;
if china_day(moon) > solstice_day {
self.new_moon_before(moon - 1.0)
} else {
Ok(moon)
}
}
fn sui(&self, solstice: f64) -> Result<(Vec<Month>, i64, f64), EngineError> {
let next_solstice = self.sun_at(270.0, solstice + TROPICAL_YEAR)?;
let end = china_day(self.month_eleven(next_solstice)?);
let mut moon = self.month_eleven(solstice)?;
let mut starts = vec![china_day(moon)];
loop {
moon = self.new_moon_near(moon + SYNODIC_MONTH)?;
let day = china_day(moon);
if day >= end {
break;
}
starts.push(day);
}
let mut major_days = vec![china_day(solstice)];
let mut term = solstice;
for k in 1..12 {
term = self.sun_at(f64::from((270 + 30 * k) % 360), term + 30.44)?;
major_days.push(china_day(term));
}
let (first_year, ..) =
UtcInstant::from_micros((starts[0] - UNIX_EPOCH_JDN) * MICROS_PER_DAY).civil();
let mut ends: Vec<i64> = starts[1..].to_vec();
ends.push(end);
let mut leap_pending = starts.len() == 13;
let mut number = 11u8;
let mut year = first_year;
let mut months = Vec::with_capacity(starts.len());
for (i, (&start, &month_end)) in starts.iter().zip(&ends).enumerate() {
let has_major = major_days.iter().any(|&d| start <= d && d < month_end);
let leap = i > 0 && leap_pending && !has_major;
if leap {
leap_pending = false;
} else if i > 0 {
number = number % 12 + 1;
if number == 1 {
year = first_year + 1;
}
}
months.push(Month {
start,
end: month_end,
number,
leap,
year,
});
}
Ok((months, end, next_solstice))
}
fn equation_of_time(&self, jd: f64) -> Result<f64, EngineError> {
let kernel = require_kernel(self.kernels, jd)?;
let t = Time::from_ut1(jd);
let orientation = Orientation::with_nutation(&t, iau2000b_radians(t.tt()));
let position = apparent(kernel, &observe(kernel, SUN, &t)?, &t)?;
let equatorial = mxv(&orientation.m, &position);
let ra_hours = equatorial[1].atan2(equatorial[0]).rem_euclid(TAU) * 24.0 / TAU;
let apparent_hours = orientation.gast_hours - ra_hours + 12.0;
let mean_hours = (jd + 0.5).rem_euclid(1.0) * 24.0;
Ok(((apparent_hours - mean_hours + 12.0).rem_euclid(24.0) - 12.0) * 60.0)
}
}
struct Month {
start: i64,
end: i64,
number: u8,
leap: bool,
year: i32,
}
fn crossing(
f: impl Fn(f64) -> Result<f64, EngineError>,
target: f64,
guess: f64,
rate: f64,
) -> Result<f64, EngineError> {
let mut t0 = guess;
let mut d0 = wrap180(f(t0)? - target);
let mut t1 = t0 - d0 / rate;
for _ in 0..50 {
if (t1 - t0).abs() < EPSILON_DAYS {
break;
}
let d1 = wrap180(f(t1)? - target);
let slope = (d1 - d0) / (t1 - t0);
let slope = if slope.is_finite() && slope > rate / 4.0 {
slope
} else {
rate
};
(t0, d0) = (t1, d1);
t1 -= d1 / slope;
}
Ok(t1)
}
pub fn chinese_calendar(
kernels: &KernelSet,
instant: UtcInstant,
) -> Result<Option<ChineseCalendar>, EngineError> {
match calendar(kernels, instant) {
Err(EngineError::OutOfRange { .. }) => Ok(None),
other => other.map(Some),
}
}
fn calendar(kernels: &KernelSet, instant: UtcInstant) -> Result<ChineseCalendar, EngineError> {
let sky = Sky { kernels };
let jd = instant.julian_day();
let mut solar_terms = Vec::new();
let mut term = sky.sun_before(f64::from(LICHUN), jd)?;
let mut longitude = LICHUN;
loop {
let jie = longitude % 30 == 15;
solar_terms.push(SolarTerm {
name: TERM_NAMES[usize::from(longitude / 15)].to_string(),
longitude,
jie,
utc: instant_of(term).isoformat(),
});
if jie && term > jd {
break;
}
longitude = (longitude + 15) % 360;
term = sky.sun_at(f64::from(longitude), term + 15.2)?;
}
let day = china_day(jd);
let solstice = sky.sun_before(270.0, jd)?;
let (mut months, mut end, mut next_solstice) = sky.sui(solstice)?;
if day - DATE_MARGIN < months[0].start {
let (mut earlier, ..) = sky.sui(sky.sun_at(270.0, solstice - TROPICAL_YEAR)?)?;
earlier.append(&mut months);
months = earlier;
}
while day + DATE_MARGIN >= end {
let (mut later, later_end, later_solstice) = sky.sui(next_solstice)?;
months.append(&mut later);
(end, next_solstice) = (later_end, later_solstice);
}
let lunar_months = months
.into_iter()
.filter(|m| m.start <= day + DATE_MARGIN && m.end > day - DATE_MARGIN)
.map(|m| LunarMonth {
year: m.year,
month: m.number,
leap: m.leap,
start: date_of(m.start),
days: (m.end - m.start) as u8,
})
.collect();
Ok(ChineseCalendar {
equation_of_time: sky.equation_of_time(jd)?,
solar_terms,
lunar_months,
})
}