// SunCalc 2.0.1 Meeus ch.15 solvers. See THIRD_PARTY.md.

///|
pub(all) enum HorizonState {
  Crosses
  AlwaysAbove
  AlwaysBelow
} derive(Eq, Debug)

///|
pub(all) struct SolarEvent {
  angle : Double
  rise_name : String
  set_name : String
  rise : Instant?
  set : Instant?
  state : HorizonState
} derive(Eq, Debug)

///|
pub(all) struct SunTimes {
  solar_noon : Instant
  nadir : Instant
  events : Array[SolarEvent]
  state : HorizonState
} derive(Eq, Debug)

///|
fn wrap_pi(a : Double) -> Double {
  a - 2.0 * @math.PI * js_round(a / (2.0 * @math.PI))
}

///|
fn solar_transit(start : Double, lw : Double) -> Double {
  let mut dt = start
  for i = 0; i < 3; i = i + 1 {
    let h = wrap_pi(sidereal_time(dt, lw) - sun_coords(to_tt(dt)).ra)
    dt -= h / (2.0 * @math.PI)
  }
  dt
}

///|
fn solar_crossing(
  h0 : Double,
  dt : Double,
  sign : Double,
  lw : Double,
  phi : Double,
  dec_t : Double,
) -> Double? {
  let cos_h0 = (@math.sin(h0) - @math.sin(phi) * @math.sin(dec_t)) /
    (@math.cos(phi) * @math.cos(dec_t))
  if cos_h0 < -1.0 || cos_h0 > 1.0 {
    return None
  }
  let mut d = dt + sign * @math.acos(cos_h0) / (2.0 * @math.PI)
  for i = 0; i < 2; i = i + 1 {
    let c = sun_coords(to_tt(d))
    let h = wrap_pi(sidereal_time(d, lw) - c.ra)
    let alt = altitude(h, phi, c.dec)
    let sin_h = @math.cos(phi) * @math.cos(c.dec) * @math.sin(h)
    if sin_h.abs() < 0.000001 {
      break
    }
    d += (alt - h0) / (2.0 * @math.PI * sin_h)
  }
  Some(d)
}

///|
/// Six standard event pairs. Missing polar events are None, never invalid dates.
pub fn sun_times_with(
  date : Instant,
  site : Observer,
  config : SolarConfig,
) -> SunTimes {
  let lw = Rad * -site.lng
  let phi = Rad * site.lat
  let d = js_round(js_round(to_days(date)) - 0.0009 - lw / (2.0 * @math.PI))
  let dt = solar_transit(d + 0.0009 + lw / (2.0 * @math.PI), lw)
  let dec = sun_coords(to_tt(dt)).dec
  let events = []
  let configured = standard_angles()
  for extra in config.extra {
    configured.push((extra.angle, extra.rise_name, extra.set_name))
  }
  let dh = -2.076 * config.height.sqrt() / 60.0
  for entry in configured {
    let (angle, rise_name, set_name) = entry
    let rise = solar_crossing((angle + dh) * Rad, dt, -1.0, lw, phi, dec).map(
      from_days,
    )
    let set = solar_crossing((angle + dh) * Rad, dt, 1.0, lw, phi, dec).map(
      from_days,
    )
    let state = if rise is Some(_) {
      Crosses
    } else if altitude(0.0, phi, dec) > (angle + dh) * Rad {
      AlwaysAbove
    } else {
      AlwaysBelow
    }
    events.push({ angle, rise_name, set_name, rise, set, state, })
  }
  {
    solar_noon: from_days(dt),
    nadir: from_days(dt - 0.5),
    events,
    state: events[0].state,
  }
}

///|
/// Lookup a standard or configured event by its case-sensitive upstream name.
pub fn SunTimes::event(self : SunTimes, name : String) -> Instant? {
  for e in self.events {
    if e.rise_name == name {
      return e.rise
    }
    if e.set_name == name {
      return e.set
    }
  }
  None
}

///|
/// Six standard pairs at sea-level horizon.
pub fn sun_times(date : Instant, site : Observer) -> SunTimes {
  sun_times_with(date, site, { height: 0.0, extra: [], })
}