// SunCalc 2.0.1: quadratic two-hour scan followed by Newton refinement.

///|
pub(all) struct MoonTimes {
  rise : Instant?
  set : Instant?
  state : HorizonState
} derive(Eq, Debug)

///|
fn moon_height(date : Instant, site : Observer) -> Double {
  let p = moon_position(date, site)
  p.altitude + 0.2725 * @math.asin(EarthRadius / p.distance) / Rad + 0.09
}

///|
fn hours_later(date : Instant, hours : Double) -> Instant {
  { ms: @math.trunc(date.ms + hours * DayMs / 24.0), }
}

///|
fn refine_moon_cross(ms : Double, site : Observer) -> Instant {
  let mut t = ms
  for i = 0; i < 2; i = i + 1 {
    let h = moon_height({ ms: @math.trunc(t), }, site)
    let dh = (
        moon_height({ ms: @math.trunc(t + 30000.0), }, site) -
        moon_height({ ms: @math.trunc(t - 30000.0), }, site)
      ) /
      60000.0
    // A grazing root may have zero slope. Keep the finite sampler/refinement value
    // rather than returning an invalid Instant or taking an unbounded Newton step.
    let step = h / dh
    if !finite(step) || dh.abs() < 1.0e-15 || step.abs() > 7200000.0 {
      break
    }
    t -= step
  }
  { ms: @math.trunc(t), }
}

///|
/// Scan the UTC calendar day containing date, independent of host timezone.
/// One-sided days keep the missing event as None. Does not accept a local civil day.
pub fn moon_times(date : Instant, site : Observer) -> MoonTimes {
  let t = date.utc_midnight()
  let mut h0 = moon_height(t, site)
  let mut rise : Double? = None
  let mut set : Double? = None
  let mut h_max = h0
  for i = 1; i <= 24; i = i + 2 {
    let h1 = moon_height(hours_later(t, i.to_double()), site)
    let h2 = moon_height(hours_later(t, (i + 1).to_double()), site)
    h_max = h_max.max(h1).max(h2)
    let (roots, x1, x2, ye) = interval_roots(h0, h1, h2)
    if roots == 1 {
      if h0 < 0.0 {
        rise = Some(i.to_double() + x1)
      } else {
        set = Some(i.to_double() + x1)
      }
    } else if roots == 2 {
      rise = Some(i.to_double() + (if ye < 0.0 { x2 } else { x1 }))
      set = Some(i.to_double() + (if ye < 0.0 { x1 } else { x2 }))
    }
    if rise is Some(_) && set is Some(_) {
      break
    }
    h0 = h2
  }
  let state = if rise is Some(_) || set is Some(_) {
    Crosses
  } else if h_max > 0.0 {
    AlwaysAbove
  } else {
    AlwaysBelow
  }
  {
    rise: rise.map(fn(h) { refine_moon_cross(hours_later(t, h).ms, site) }),
    set: set.map(fn(h) { refine_moon_cross(hours_later(t, h).ms, site) }),
    state,
  }
}