//
//  MoonPhaseCalculator.swift
//  Shared between MoonWatch and MoonWatchWidget.
//

import Foundation

public enum MoonPhaseCalculator: Sendable {
    /// Mean synodic month length in days (Meeus).
    public static let synodicMonth = 29.530588853

    /// Julian date at Unix epoch (1970-01-01 00:00 UTC).
    private static let julianDayUnixEpoch = 24_405_87.5

    /// Known new moon reference: 2000-01-06 18:14 UTC, JD 2451550.09765.
    private static let julianDayKnownNewMoon = 2_451_550.09765

    public static func julianDay(for date: Date) -> Double {
        date.timeIntervalSince1970 / 86_400.0 + julianDayUnixEpoch
    }

    /// Days since the reference new moon, wrapped to `[0, synodicMonth)`.
    public static func moonAgeDays(for date: Date) -> Double {
        let jd = julianDay(for: date)
        var age = jd - julianDayKnownNewMoon
        age = age.truncatingRemainder(dividingBy: synodicMonth)
        if age < 0 { age += synodicMonth }
        return age
    }

    /// Normalized phase in `[0, 1)`; 0 is new moon.
    public static func normalizedPhase(for date: Date) -> Double {
        moonAgeDays(for: date) / synodicMonth
    }

    /// Illuminated fraction of the apparent disk (simple geometric model).
    public static func illuminatedFraction(for date: Date) -> Double {
        let t = normalizedPhase(for: date)
        let angle = 2 * Double.pi * t
        return (1 - cos(angle)) / 2
    }

    public static func namedPhase(normalizedPhase t: Double) -> MoonPhaseName {
        let x = t.truncatingRemainder(dividingBy: 1)
        let u = x < 0 ? x + 1 : x
        // Cardinal phases (New, First Quarter, Full, Last Quarter) are astronomical
        // moments, not equal eighths of the cycle. Treat them as narrow windows of
        // 1/16 of the cycle (~1.85 days) centered at 0, 0.25, 0.5, 0.75; the four
        // intermediate phases fill the 3/16 gaps between them.
        let step = u * 32
        switch step {
        case ..<1, 31...:    return .newMoon
        case 1..<7:          return .waxingCrescent
        case 7..<9:          return .firstQuarter
        case 9..<15:         return .waxingGibbous
        case 15..<17:        return .fullMoon
        case 17..<23:        return .waningGibbous
        case 23..<25:        return .thirdQuarter
        default:             return .waningCrescent
        }
    }

    /// Returns the next phase transition: which phase comes next and how many
    /// days until the boundary is crossed.
    public static func nextPhaseTransition(normalizedPhase t: Double) -> (phase: MoonPhaseName, daysAway: Double) {
        let raw = t.truncatingRemainder(dividingBy: 1)
        let u   = raw < 0 ? raw + 1 : raw
        let step = u * 32

        // Each entry is (boundary in 32nds, the phase that begins at that boundary)
        let transitions: [(Double, MoonPhaseName)] = [
            (1,  .waxingCrescent),
            (7,  .firstQuarter),
            (9,  .waxingGibbous),
            (15, .fullMoon),
            (17, .waningGibbous),
            (23, .thirdQuarter),
            (25, .waningCrescent),
            (31, .newMoon),
        ]
        for (boundary, phase) in transitions where step < boundary {
            return (phase, (boundary / 32.0 - u) * synodicMonth)
        }
        // Late new-moon window (step ≥ 31): next is waxing crescent at 1/32 next cycle
        return (.waxingCrescent, (1.0 + 1.0 / 32.0 - u) * synodicMonth)
    }

    public static func snapshot(for date: Date) -> MoonPhaseSnapshot {
        let age  = moonAgeDays(for: date)
        let norm = age / synodicMonth
        let (nextPhase, daysToNext) = nextPhaseTransition(normalizedPhase: norm)
        return MoonPhaseSnapshot(
            date: date,
            normalizedPhase: norm,
            moonAgeDays: age,
            namedPhase: namedPhase(normalizedPhase: norm),
            illuminatedFraction: illuminatedFraction(for: date),
            nextPhaseName: nextPhase,
            daysToNextPhase: daysToNext
        )
    }
}
