///|
pub struct SolarAngle {
  priv angle : Double
  priv rise_name : String
  priv set_name : String
} derive(Eq, Debug)

///|
/// Config is immutable to callers; constructor copies the extra-angle array.
pub struct SolarConfig {
  priv height : Double
  priv extra : Array[SolarAngle]
} derive(Eq, Debug)

///|
fn standard_angles() -> Array[(Double, String, String)] {
  [
    (-0.833, "sunrise", "sunset"),
    (-0.3, "sunriseEnd", "sunsetStart"),
    (-6.0, "dawn", "dusk"),
    (-12.0, "nauticalDawn", "nauticalDusk"),
    (-18.0, "nightEnd", "night"),
    (6.0, "goldenHourEnd", "goldenHour"),
  ]
}

///|
/// A geometric solar-centre angle, not the refracted altitude of sun_position.
pub fn solar_angle(
  angle : Double,
  rise_name : String,
  set_name : String,
) -> Result[SolarAngle, InputError] {
  if !(angle > -90.0 && angle < 90.0) ||
    rise_name.length() == 0 ||
    set_name.length() == 0 ||
    rise_name.length() > 64 ||
    set_name.length() > 64 ||
    rise_name == set_name {
    return Err(InvalidAngle)
  }
  Ok({ angle, rise_name, set_name, })
}

///|
/// Height above the visible horizon, 0..10000 m; up to 32 extra angle pairs.
/// Duplicate/reserved names are rejected instead of overwriting standard results.
pub fn solar_config(
  height_m : Double,
  extra : Array[SolarAngle],
) -> Result[SolarConfig, InputError] {
  if !(height_m >= 0.0 && height_m <= 10000.0) {
    return Err(InvalidHeight)
  }
  if extra.length() > 32 {
    return Err(TooManyEvents)
  }
  let names : Array[String] = ["solarNoon", "nadir", "alwaysUp", "alwaysDown"]
  for row in standard_angles() {
    names.push(row.1)
    names.push(row.2)
  }
  for e in extra {
    if names.contains(e.rise_name) || names.contains(e.set_name) {
      return Err(InvalidAngle)
    }
    names.push(e.rise_name)
    names.push(e.set_name)
  }
  Ok({ height: height_m, extra: extra.copy(), })
}