use std::panic::AssertUnwindSafe;
use stem_branch::{
delta_t_for_year, gregorian_to_lunisolar, jd_from_ymd, solar_ecliptic_state,
solar_term_for_longitude, CivilDate, EARTHLY_BRANCHES, HEAVENLY_STEMS,
};
use crate::{ChronoError, PosixNs, RenderZone};
const UNIX_EPOCH_JD: f64 = 2_440_587.5;
const LICHUN_LONGITUDE: f64 = 315.0;
#[derive(Debug, Clone, serde::Serialize)]
pub struct LunisolarReading {
pub lunar_year: i32,
pub lunar_month: u8,
pub lunar_day: u8,
pub is_leap_month: bool,
pub year_pillar: String,
pub month_pillar: String,
pub day_pillar: String,
pub hour_pillar: String,
pub solar_longitude_deg: f64,
pub solar_term: String,
pub civil_local: String,
pub assumptions: Vec<String>,
}
pub fn render(
instant: PosixNs,
zone: &RenderZone,
longitude: Option<f64>,
) -> Result<LunisolarReading, ChronoError> {
let ts = jiff::Timestamp::from_nanosecond(instant.0)
.map_err(|e| ChronoError::Render(e.to_string()))?;
let offset = match zone {
RenderZone::Utc => jiff::tz::Offset::UTC,
RenderZone::Fixed(o) => *o,
RenderZone::Named(tz) => tz.to_offset(ts),
};
let dt = offset.to_datetime(ts);
let (year, month, day) = (i32::from(dt.year()), dt.month() as u8, dt.day() as u8);
let civil_hour = i64::from(dt.hour());
let civil_minute = i64::from(dt.minute());
#[allow(clippy::cast_precision_loss)]
let jd_ut = instant.0 as f64 / 1e9 / 86_400.0 + UNIX_EPOCH_JD;
let jde_tt = jd_ut + delta_t_for_year(f64::from(year)) / 86_400.0;
let lambda = normalize_deg(solar_ecliptic_state(jde_tt).apparent_longitude_degrees);
let solar_term = solar_term_for_longitude(lambda).to_string();
let pillar_year = if month == 1 || (month == 2 && lambda < LICHUN_LONGITUDE) {
year - 1
} else {
year
};
let year_stem = (pillar_year - 1984).rem_euclid(10) as usize;
let year_pillar = pillar(year_stem, (pillar_year - 1984).rem_euclid(12) as usize);
let sector = (((lambda - LICHUN_LONGITUDE).rem_euclid(360.0)) / 30.0).floor() as usize;
let month_branch = (2 + sector) % 12;
let first_month_stem = (year_stem * 2 + 2) % 10; let month_pillar = pillar((first_month_stem + sector) % 10, month_branch);
let base_jdn = noon_jdn(year, month, day);
let day_jdn = base_jdn + i64::from(civil_hour == 23);
let day_cycle = (day_jdn + 49).rem_euclid(60);
let day_pillar = pillar((day_cycle % 10) as usize, (day_cycle % 12) as usize);
let ref_lon = f64::from(offset.seconds()) / 3600.0 * 15.0;
let (eff_hour, solar_note) = match longitude {
Some(lon) => {
let corr_min = ((lon - ref_lon) * 4.0).round() as i64;
let total = (civil_hour * 60 + civil_minute + corr_min).rem_euclid(24 * 60);
(
total / 60,
format!(
"hour pillar uses local MEAN solar time (longitude {lon:.4}°E vs meridian {ref_lon:.1}°E, {corr_min:+} min); equation of time NOT applied, and the day/year/month pillars keep civil meridian time"
),
)
}
None => (
civil_hour,
"hour pillar uses civil time at the meridian (no longitude → true solar time not applied)".to_string(),
),
};
let hour_branch = ((eff_hour + 1) / 2).rem_euclid(12) as usize;
let hour_day_cycle = (base_jdn + i64::from(eff_hour == 23) + 49).rem_euclid(60);
let hour_stem = (hour_day_cycle as usize % 10 % 5 * 2 + hour_branch) % 10;
let hour_pillar = pillar(hour_stem, hour_branch);
let civil = CivilDate {
year,
month: u32::from(month),
day: u32::from(day),
};
let lunar = std::panic::catch_unwind(AssertUnwindSafe(|| gregorian_to_lunisolar(civil)))
.map_err(|_| {
ChronoError::Render(format!(
"lunisolar conversion is outside the supported range for {year}-{month:02}-{day:02}"
))
})?;
let assumptions = vec![
format!(
"Chinese reading computed for the {ref_lon:.1}°E meridian (UTC offset {} h); a different tradition (e.g. Vietnam UTC+7, Korea UTC+9) can differ by a day or a leap month",
offset.seconds() / 3600
),
"year pillar via 立春 and month pillar via the 12 節 (apparent solar longitude); the lunar DATE uses the 正月初一 new-year boundary (中氣 leap-month rule), so the year pillar and lunar year may differ near 立春".to_string(),
solar_note,
];
Ok(LunisolarReading {
lunar_year: lunar.year,
lunar_month: lunar.month as u8,
lunar_day: lunar.day as u8,
is_leap_month: lunar.is_leap_month,
year_pillar,
month_pillar,
day_pillar,
hour_pillar,
solar_longitude_deg: lambda,
solar_term,
civil_local: instant
.render(zone)
.unwrap_or_else(|| "<out of civil range>".to_string()),
assumptions,
})
}
fn pillar(stem: usize, branch: usize) -> String {
format!(
"{}{}",
HEAVENLY_STEMS[stem % 10],
EARTHLY_BRANCHES[branch % 12]
)
}
fn normalize_deg(deg: f64) -> f64 {
deg.rem_euclid(360.0)
}
fn noon_jdn(year: i32, month: u8, day: u8) -> i64 {
(jd_from_ymd(year, u32::from(month), u32::from(day)) + 0.5).floor() as i64
}