///|
pub enum ObservationKind {
  Range
  RangeRate
  Azimuth
  Elevation
} derive(Debug, Eq)

///|
pub enum ObservationQuality {
  Good
  Suspect
  Invalid
} derive(Debug, Eq)

///|
pub struct Observation {
  epoch : Epoch
  station : GroundStation
  kind : ObservationKind
  value : Double
  predicted : Double
  sigma : Double
  quality : ObservationQuality
} derive(Debug, Eq)

///|
pub struct ObservationBatch {
  name : String
  observations : Array[Observation]
} derive(Debug, Eq)

///|
pub fn observation_kind_name(kind : ObservationKind) -> String {
  match kind {
    Range => "range"
    RangeRate => "range-rate"
    Azimuth => "azimuth"
    Elevation => "elevation"
  }
}

///|
pub fn all_observation_kinds() -> Array[ObservationKind] {
  [Range, RangeRate, Azimuth, Elevation]
}

///|
pub fn invalid_observation_quality() -> ObservationQuality {
  ObservationQuality::Invalid
}

///|
pub fn observation_kind_is_angular(kind : ObservationKind) -> Bool {
  match kind {
    Azimuth | Elevation => true
    _ => false
  }
}

///|
pub fn observation_kind_is_dynamic(kind : ObservationKind) -> Bool {
  match kind {
    RangeRate => true
    _ => false
  }
}

///|
pub fn Observation::new(
  epoch : Epoch,
  station : GroundStation,
  kind : ObservationKind,
  value : Double,
  sigma : Double,
) -> Observation {
  let quality = if sigma <= 0.0 || value.is_nan() || value.is_inf() {
    ObservationQuality::Invalid
  } else {
    ObservationQuality::Good
  }
  { epoch, station, kind, value, predicted: value, sigma: sigma.abs(), quality }
}

///|
pub fn Observation::with_quality(
  observation : Observation,
  quality : ObservationQuality,
) -> Observation {
  { ..observation, quality, }
}

///|
pub fn Observation::with_prediction(
  observation : Observation,
  predicted : Double,
) -> Observation {
  let quality = if predicted.is_nan() || predicted.is_inf() {
    ObservationQuality::Invalid
  } else {
    observation.quality
  }
  { ..observation, predicted, quality }
}

///|
pub fn Observation::is_valid(observation : Observation) -> Bool {
  observation.quality is Good || observation.quality is Suspect
}

///|
pub fn Observation::residual(observation : Observation) -> Double {
  observation.value - observation.predicted
}

///|
pub fn Observation::normalized_residual(observation : Observation) -> Double {
  if observation.sigma <= 0.0 {
    0.0
  } else {
    observation.residual() / observation.sigma
  }
}

///|
pub fn Observation::weight(observation : Observation) -> Double {
  if !observation.is_valid() || observation.sigma <= 0.0 {
    0.0
  } else {
    1.0 / (observation.sigma * observation.sigma)
  }
}

///|
pub fn ObservationBatch::new(name : String) -> ObservationBatch {
  { name, observations: [] }
}

///|
pub fn ObservationBatch::add(
  batch : ObservationBatch,
  observation : Observation,
) -> ObservationBatch {
  let observations = batch.observations.copy()
  observations.push(observation)
  { ..batch, observations, }
}

///|
pub fn ObservationBatch::sort_by_epoch(
  batch : ObservationBatch,
) -> ObservationBatch {
  let observations = batch.observations.copy()
  for i in 0.. Int {
  batch.observations.filter(item => item.is_valid()).length()
}

///|
pub fn ObservationBatch::invalid_count(batch : ObservationBatch) -> Int {
  batch.observations.length() - batch.valid_count()
}

///|
pub fn ObservationBatch::span_s(batch : ObservationBatch) -> Double {
  if batch.observations.length() < 2 {
    0.0
  } else {
    let sorted = batch.sort_by_epoch()
    sorted.observations[sorted.observations.length() - 1].epoch.seconds_since_j2000 -
    sorted.observations[0].epoch.seconds_since_j2000
  }
}

///|
pub fn ObservationBatch::by_kind(
  batch : ObservationBatch,
  kind : ObservationKind,
) -> Array[Observation] {
  batch.observations.filter(item => item.kind == kind)
}

///|
pub fn ObservationBatch::residual_rms(batch : ObservationBatch) -> Double {
  let valid = batch.observations.filter(item => item.is_valid())
  if valid.length() == 0 {
    0.0
  } else {
    let total = valid.fold(init=0.0, (sum, item) => {
      sum + item.residual() * item.residual()
    })
    (total / Double::from_int(valid.length())).sqrt()
  }
}

///|
pub fn ObservationBatch::weighted_residual_rms(
  batch : ObservationBatch,
) -> Double {
  let valid = batch.observations.filter(item => {
    item.is_valid() && item.weight() > 0.0
  })
  if valid.length() == 0 {
    0.0
  } else {
    let weighted = valid.fold(init=0.0, (sum, item) => {
      sum + item.weight() * item.residual() * item.residual()
    })
    let weights = valid.fold(init=0.0, (sum, item) => sum + item.weight())
    (weighted / weights.max(1.0e-30)).sqrt()
  }
}

///|
pub fn observation_prediction(
  station : GroundStation,
  epoch : Epoch,
  state : StateVector,
  kind : ObservationKind,
) -> Double {
  let look = look_angle_from_eci(station.location, epoch, state.position_km)
  match kind {
    Range => look.range_km
    RangeRate => state.velocity_km_s.dot(state.position_km.unit())
    Azimuth => look.azimuth_rad
    Elevation => look.elevation_rad
  }
}

///|
pub fn observe_state(
  station : GroundStation,
  epoch : Epoch,
  state : StateVector,
  kind : ObservationKind,
  bias? : Double = 0.0,
  sigma? : Double = 1.0,
) -> Observation {
  let predicted = observation_prediction(station, epoch, state, kind)
  let value = predicted + bias
  let quality = if sigma <= 0.0 || predicted.is_nan() || predicted.is_inf() {
    ObservationQuality::Invalid
  } else {
    ObservationQuality::Good
  }
  { epoch, station, kind, value, predicted, sigma: sigma.abs(), quality }
}

///|
pub fn observation_residual(
  observation : Observation,
  state : StateVector,
) -> Double {
  observation.value -
  observation_prediction(
    observation.station,
    observation.epoch,
    state,
    observation.kind,
  )
}

///|
pub fn mark_observation_outlier(
  observation : Observation,
  limit_sigma : Double,
) -> Observation {
  if observation.normalized_residual().abs() > limit_sigma.abs() {
    observation.with_quality(Suspect)
  } else {
    observation
  }
}

///|
pub fn observation_quality_score(observation : Observation) -> Double {
  match observation.quality {
    Good => 1.0
    Suspect => 0.5
    Invalid => 0.0
  }
}