///|
/// Validation failures for a non-destructive constant-gain recommendation.
pub enum NormalizationPlanError {
  MissingIntegratedLoudness
  InvalidTargetLufs(Double)
  InvalidTruePeakCeilingDbtp(Double)
  InvalidMeasuredLevels
  UnsafeNormalizationMath
}

///|
/// Constant-gain advice constrained by an explicit true-peak ceiling.
/// This value is informational: it never changes or limits audio samples.
/// Predicted loudness holds the original gate membership fixed. Gain can move
/// blocks across the absolute gate, so processed audio MUST be remeasured.
pub struct NormalizationPlan {
  priv target_lufs : Double
  priv true_peak_ceiling_dbtp : Double
  priv requested_gain_db : Double
  priv peak_limited_gain_db : Double
  priv applied_gain_db : Double
  priv linear_gain : Double
  priv predicted_integrated_lufs : Double
  priv predicted_true_peak_dbtp : Double
  priv peak_constrained : Bool
}

///|
pub fn NormalizationPlan::target_lufs(self : NormalizationPlan) -> Double {
  self.target_lufs
}

///|
pub fn NormalizationPlan::true_peak_ceiling_dbtp(
  self : NormalizationPlan,
) -> Double {
  self.true_peak_ceiling_dbtp
}

///|
pub fn NormalizationPlan::requested_gain_db(self : NormalizationPlan) -> Double {
  self.requested_gain_db
}

///|
pub fn NormalizationPlan::peak_limited_gain_db(
  self : NormalizationPlan,
) -> Double {
  self.peak_limited_gain_db
}

///|
pub fn NormalizationPlan::applied_gain_db(self : NormalizationPlan) -> Double {
  self.applied_gain_db
}

///|
/// Fixed-gate estimate, not an exact remeasurement of gained audio.
/// Absolute-gate membership can change after gain; remeasure the output.
pub fn NormalizationPlan::predicted_integrated_lufs(
  self : NormalizationPlan,
) -> Double {
  self.predicted_integrated_lufs
}

///|
pub fn NormalizationPlan::predicted_true_peak_dbtp(
  self : NormalizationPlan,
) -> Double {
  self.predicted_true_peak_dbtp
}

///|
pub fn NormalizationPlan::peak_constrained(self : NormalizationPlan) -> Bool {
  self.peak_constrained
}

///|
fn make_normalization_plan(
  measured_lufs : Double,
  measured_dbtp : Double,
  target_lufs : Double,
  ceiling_dbtp : Double,
) -> Result[NormalizationPlan, NormalizationPlanError] {
  if target_lufs - target_lufs != 0.0 ||
    target_lufs < -70.0 ||
    target_lufs > 24.0 {
    return Err(InvalidTargetLufs(target_lufs))
  }
  if ceiling_dbtp - ceiling_dbtp != 0.0 ||
    ceiling_dbtp < -100.0 ||
    ceiling_dbtp > 24.0 {
    return Err(InvalidTruePeakCeilingDbtp(ceiling_dbtp))
  }
  if measured_lufs - measured_lufs != 0.0 ||
    measured_dbtp - measured_dbtp != 0.0 {
    return Err(InvalidMeasuredLevels)
  }
  let requested_gain = target_lufs - measured_lufs
  let peak_limited_gain = ceiling_dbtp - measured_dbtp
  let peak_constrained = peak_limited_gain < requested_gain
  let applied_gain = if peak_constrained {
    peak_limited_gain
  } else {
    requested_gain
  }
  let linear_gain = @math.pow(10.0, applied_gain / 20.0)
  let predicted_lufs = measured_lufs + applied_gain
  let predicted_dbtp = measured_dbtp + applied_gain
  if requested_gain - requested_gain != 0.0 ||
    peak_limited_gain - peak_limited_gain != 0.0 ||
    applied_gain - applied_gain != 0.0 ||
    linear_gain - linear_gain != 0.0 ||
    linear_gain <= 0.0 ||
    predicted_lufs - predicted_lufs != 0.0 ||
    predicted_dbtp - predicted_dbtp != 0.0 {
    return Err(UnsafeNormalizationMath)
  }
  Ok({
    target_lufs,
    true_peak_ceiling_dbtp: ceiling_dbtp,
    requested_gain_db: requested_gain,
    peak_limited_gain_db: peak_limited_gain,
    applied_gain_db: applied_gain,
    linear_gain,
    predicted_integrated_lufs: predicted_lufs,
    predicted_true_peak_dbtp: predicted_dbtp,
    peak_constrained,
  })
}

///|
/// Recommends constant gain only; it does not normalize or limit the stream.
pub fn MeasurementReport::normalization_plan(
  self : MeasurementReport,
  target_lufs : Double,
  ceiling_dbtp : Double,
) -> Result[NormalizationPlan, NormalizationPlanError] {
  match (self.integrated_lufs, self.true_peak_dbtp()) {
    (Some(lufs), Some(dbtp)) =>
      make_normalization_plan(lufs, dbtp, target_lufs, ceiling_dbtp)
    _ => Err(MissingIntegratedLoudness)
  }
}

///|
pub fn AggregateReport::normalization_plan(
  self : AggregateReport,
  target_lufs : Double,
  ceiling_dbtp : Double,
) -> Result[NormalizationPlan, NormalizationPlanError] {
  match (self.integrated_lufs(), self.true_peak_dbtp()) {
    (Some(lufs), Some(dbtp)) =>
      make_normalization_plan(lufs, dbtp, target_lufs, ceiling_dbtp)
    _ => Err(MissingIntegratedLoudness)
  }
}

///|
/// Positive finite amplitude multiplier corresponding to applied_gain_db.
/// Does not apply gain, change gating, or guarantee exact target loudness.
pub fn NormalizationPlan::linear_gain(self : NormalizationPlan) -> Double {
  self.linear_gain
}

///|
test "normalization rejects invalid measured levels and unsafe finite math" {
  assert_true(
    make_normalization_plan(0.0 / 0.0, 0.0, -23.0, -1.0)
    is Err(InvalidMeasuredLevels),
  )
  assert_true(
    make_normalization_plan(-23.0, 1.0 / 0.0, -23.0, -1.0)
    is Err(InvalidMeasuredLevels),
  )
  assert_true(
    make_normalization_plan(-1.0e308, -1.0e308, 24.0, 24.0)
    is Err(UnsafeNormalizationMath),
  )
  assert_true(
    make_normalization_plan(1.0e308, 1.0e308, -70.0, -100.0)
    is Err(UnsafeNormalizationMath),
  )
}

///|
test "normalization equal peak allowance is not marked constrained" {
  let plan = make_normalization_plan(-23.0, -1.0, -23.0, -1.0).unwrap()
  assert_eq(plan.linear_gain(), 1.0)
  assert_eq(plan.applied_gain_db(), 0.0)
  assert_eq(plan.peak_constrained(), false)
}