//! Datums- und Zeitfunktionen der Laufzeitbibliothek.
//!
//! Serielle Datumswerte folgen der Zählweise des Vorbilds: der ganzzahlige
//! Anteil zählt Tage ab dem 30.12.1899 (Serie 2 = 1.1.1900), der
//! Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag.
//!
//! Die Zeitzone kommt von außen: [`utc_versatz`] ist der **einzige** Punkt,
//! an dem dieses Modul eine Bibliothek befragt (Entscheidung 2026-09-04,
//! design.md D1/D2). Die Kalenderrechnung darunter ist Eigenimplementierung
//! und sieht die Bibliothek nie — zwei Kalendermodelle nebeneinander wären
//! eine dauerhafte Fehlerquelle.

/// Tage zwischen dem 1.1.1970 (Unix) und dem 30.12.1899 (Serie 0).
const EPOCH_OFFSET_TAGE: i64 = 25_569;

/// Tage → (Jahr, Monat, Tag) ab Unix-Epoche (Howard Hinnant).
pub fn civil_from_days(z: i64) -> (i64, u32, u32) {
    let z = z + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = (z - era * 146_097) as u64;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
    let y = yoe as i64 + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
    (y + i64::from(m <= 2), m, d)
}

/// (Jahr, Monat, Tag) → Tage ab Unix-Epoche.
pub fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
    let y = y - i64::from(m <= 2);
    let era = if y >= 0 { y } else { y - 399 } / 400;
    let yoe = (y - era * 400) as u64;
    let mp = if m > 2 { m - 3 } else { m + 9 } as u64;
    let doy = (153 * mp + 2) / 5 + u64::from(d) - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146_097 + doe as i64 - 719_468
}

/// Gültige Kalenderangabe? (Monat 1–12, Tag im Monat.)
pub fn gueltig(y: i64, m: i64, d: i64) -> bool {
    if !(1..=12).contains(&m) || d < 1 {
        return false;
    }
    let tage = tage_im_monat(y, m as u32);
    d <= tage as i64
}

fn tage_im_monat(y: i64, m: u32) -> u32 {
    match m {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 if schaltjahr(y) => 29,
        2 => 28,
        _ => 0,
    }
}

fn schaltjahr(y: i64) -> bool {
    (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}

/// Serieller Wert aus Jahr/Monat/Tag.
pub fn serial_aus_ymd(y: i64, m: u32, d: u32) -> f64 {
    (days_from_civil(y, m, d) + EPOCH_OFFSET_TAGE) as f64
}

/// (Jahr, Monat, Tag) aus einem seriellen Wert.
pub fn ymd_aus_serial(s: f64) -> (i64, u32, u32) {
    civil_from_days(s.floor() as i64 - EPOCH_OFFSET_TAGE)
}

/// Tageszeit eines seriellen Werts als (Stunde, Minute, Sekunde).
pub fn hms_aus_serial(s: f64) -> (u32, u32, u32) {
    // Auf die Sekunde runden, damit 0.5 exakt 12:00:00 ergibt.
    let sekunden = ((s - s.floor()) * 86_400.0).round() as i64;
    let sekunden = sekunden.rem_euclid(86_400);
    (
        (sekunden / 3600) as u32,
        ((sekunden / 60) % 60) as u32,
        (sekunden % 60) as u32,
    )
}

/// Wochentag: 1 = Sonntag … 7 = Samstag.
pub fn weekday(s: f64) -> u32 {
    // Der 1.1.1970 war ein Donnerstag (Index 4 in dieser Zählung).
    let tage = s.floor() as i64 - EPOCH_OFFSET_TAGE;
    (tage.rem_euclid(7) as u32 + 4) % 7 + 1
}

/// Tageszeit als serieller Bruchteil.
pub fn serial_aus_hms(h: i64, m: i64, s: i64) -> f64 {
    (h * 3600 + m * 60 + s) as f64 / 86_400.0
}

/// Sekunden seit der Unix-Epoche (UTC) — der rohe Zeitpunkt.
pub fn jetzt_utc() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0)
}

/// UTC-Versatz der Systemzone zum Zeitpunkt `zeitpunkt` (Sekunden seit der
/// Unix-Epoche), in Sekunden. `None`, wenn sich die Zone nicht ermitteln
/// lässt — dann gilt UTC.
///
/// Der Versatz gilt **je Zeitpunkt**, nicht je Programmlauf: ein Programm,
/// das über eine Zeitumstellung hinweg läuft, folgt ihr, und ein Sprung in
/// einen anderen Teil des Jahres bekommt den dort gültigen Versatz.
///
/// Einziger Aufrufpunkt der Zeitzonenbibliothek (design.md D1/D2).
pub fn utc_versatz(zeitpunkt: i64) -> Option<i32> {
    let ts = jiff::Timestamp::from_second(zeitpunkt).ok()?;
    let zone = jiff::tz::TimeZone::try_system().ok()?;
    Some(zone.to_offset(ts).seconds())
}

/// Wohin die Zeitfunktionen rechnen.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Zeitzone {
    /// Zone des Rechners (Vorgabe).
    #[default]
    System,
    /// Fester Versatz in Sekunden — für Tests und den Korpus.
    Fest(i32),
    /// Zone nicht ermittelbar; es gilt UTC.
    Unbekannt,
}

impl Zeitzone {
    /// Versatz zu einem Zeitpunkt. Bei `System` ohne ermittelbare Zone
    /// fällt der Wert auf 0 (UTC) zurück. BASIC und Einbetter verwenden
    /// [`crate::builtins::RtState::zeitpunkt`] für Zeit und aktuellen Status.
    pub fn versatz(&self, zeitpunkt: i64) -> i32 {
        match self {
            Zeitzone::System => utc_versatz(zeitpunkt).unwrap_or(0),
            Zeitzone::Fest(n) => *n,
            Zeitzone::Unbekannt => 0,
        }
    }

    /// Momentaufnahme: `System`, wenn die Zone lesbar ist, sonst `Unbekannt`.
    /// Die Laufzeit behält `System` als Quelle, damit sie nach Ausfällen
    /// erneut auflösen kann; `RtState::zeitpunkt` liefert den aktuellen Status.
    pub fn ermitteln() -> Zeitzone {
        match utc_versatz(jetzt_utc()) {
            Some(_) => Zeitzone::System,
            None => Zeitzone::Unbekannt,
        }
    }
}

/// Sekunden seit der Unix-Epoche in **Ortszeit** gelesen: der rohe
/// Zeitpunkt plus Zonenversatz plus programmeigener Versatz. Alles
/// Weitere (`DATE$`, `TIME$`, `TIMER`, `NOW`) rechnet darauf.
pub fn jetzt_sekunden(zone: Zeitzone, offset_sekunden: i64) -> i64 {
    let utc = jetzt_utc();
    utc + zone.versatz(utc) as i64 + offset_sekunden
}

/// `NOW` — aktueller Zeitpunkt als serieller Wert (Ortszeit).
pub fn jetzt_serial(zone: Zeitzone, offset_sekunden: i64) -> f64 {
    jetzt_sekunden(zone, offset_sekunden) as f64 / 86_400.0 + EPOCH_OFFSET_TAGE as f64
}

/// `DATEVALUE` — Datum aus einem Text lesen (`MM-TT-JJJJ`, `MM/TT/JJJJ`,
/// zweistelliges Jahr wird als 19xx/20xx gedeutet wie beim Vorbild).
pub fn datevalue(t: &str) -> Option<f64> {
    let teile: Vec<&str> = t
        .trim()
        .split(['-', '/', '.'])
        .filter(|p| !p.is_empty())
        .collect();
    if teile.len() != 3 {
        return None;
    }
    let m: i64 = teile[0].trim().parse().ok()?;
    let d: i64 = teile[1].trim().parse().ok()?;
    let mut y: i64 = teile[2].trim().parse().ok()?;
    if y < 100 {
        y += if y < 80 { 2000 } else { 1900 };
    }
    if !gueltig(y, m, d) {
        return None;
    }
    Some(serial_aus_ymd(y, m as u32, d as u32))
}

/// `TIMEVALUE` — Tageszeit aus einem Text lesen (`HH:MM[:SS]`).
pub fn timevalue(t: &str) -> Option<f64> {
    let teile: Vec<&str> = t.trim().split(':').collect();
    if !(2..=3).contains(&teile.len()) {
        return None;
    }
    let h: i64 = teile[0].trim().parse().ok()?;
    let m: i64 = teile[1].trim().parse().ok()?;
    let s: i64 = teile.get(2).map_or(Ok(0), |x| x.trim().parse()).ok()?;
    if !(0..24).contains(&h) || !(0..60).contains(&m) || !(0..60).contains(&s) {
        return None;
    }
    Some(serial_aus_hms(h, m, s))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serienwert_und_rueckumwandlung() {
        let s = serial_aus_ymd(1992, 3, 15);
        assert_eq!(ymd_aus_serial(s), (1992, 3, 15));
    }

    #[test]
    fn zaehlweise_des_vorbilds() {
        // Serie 2 = 1. Januar 1900.
        assert_eq!(serial_aus_ymd(1900, 1, 1), 2.0);
        assert_eq!(ymd_aus_serial(2.0), (1900, 1, 1));
    }

    #[test]
    fn wochentag_sonntag_ist_eins() {
        // 15. März 1992 war ein Sonntag.
        assert_eq!(weekday(serial_aus_ymd(1992, 3, 15)), 1);
        // 16. März 1992 war ein Montag.
        assert_eq!(weekday(serial_aus_ymd(1992, 3, 16)), 2);
    }

    #[test]
    fn tageszeit() {
        let s = serial_aus_hms(13, 45, 30);
        assert_eq!(hms_aus_serial(s), (13, 45, 30));
        assert_eq!(hms_aus_serial(0.5), (12, 0, 0));
    }

    #[test]
    fn gueltigkeitspruefung() {
        assert!(gueltig(1992, 2, 29)); // Schaltjahr
        assert!(!gueltig(1993, 2, 29));
        assert!(!gueltig(1992, 13, 1));
        assert!(!gueltig(1992, 4, 31));
    }

    #[test]
    fn versatz_liegt_im_gueltigen_bereich() {
        // Zonenversätze liegen zwischen −14 h und +14 h.
        if let Some(v) = utc_versatz(1_000_000_000) {
            assert!((-50_400..=50_400).contains(&v), "Versatz {v} unplausibel");
        }
    }

    #[test]
    fn versatz_gilt_je_zeitpunkt() {
        // 15.01.2021 und 15.07.2021 als Unix-Zeitpunkte.
        let januar = 1_610_668_800;
        let juli = 1_626_307_200;
        let (a, b) = (utc_versatz(januar), utc_versatz(juli));
        // Beide Abfragen liefern dasselbe Ergebnis-Muster: entweder kennt
        // die Maschine ihre Zone oder nicht.
        assert_eq!(a.is_some(), b.is_some());
        if let (Some(a), Some(b)) = (a, b) {
            // In einer Zone mit Sommerzeit unterscheiden sie sich um genau
            // die Umstellungsspanne, sonst gar nicht.
            let d = (a - b).abs();
            assert!(
                d == 0 || (1800..=7200).contains(&d),
                "Spanne {d} unplausibel"
            );
        }
    }

    #[test]
    fn fester_versatz_verschiebt_genau() {
        let ohne = jetzt_sekunden(Zeitzone::Unbekannt, 0);
        let mit = jetzt_sekunden(Zeitzone::Fest(3600), 0);
        // Die Systemuhr kann zwischen beiden Aufrufen eine Sekunde weiter
        // sein; die Verschiebung bleibt eine Stunde.
        assert!((mit - ohne - 3600).abs() <= 1);
    }

    #[test]
    fn programmversatz_kommt_hinzu() {
        let a = jetzt_sekunden(Zeitzone::Fest(0), 0);
        let b = jetzt_sekunden(Zeitzone::Fest(0), 120);
        assert!((b - a - 120).abs() <= 1);
    }

    #[test]
    fn text_in_serienwert() {
        assert_eq!(datevalue("03-15-1992"), Some(serial_aus_ymd(1992, 3, 15)));
        assert_eq!(datevalue("3/15/92"), Some(serial_aus_ymd(1992, 3, 15)));
        assert_eq!(datevalue("13-01-1992"), None);
        assert_eq!(timevalue("13:45:30"), Some(serial_aus_hms(13, 45, 30)));
        assert_eq!(timevalue("13:45"), Some(serial_aus_hms(13, 45, 0)));
        assert_eq!(timevalue("25:00"), None);
    }
}
