///|
/// Converts unavailable or non-finite quantities to explicit JSON null.
fn report_json_number(value : Double?) -> Json {
  match value {
    Some(number) if number - number == 0.0 => Json::number(number)
    _ => Json::null()
  }
}

///|
fn report_integrated_gate_json(gate : IntegratedGateReport?) -> Json {
  match gate {
    None => Json::null()
    Some(gate) =>
      Json::object({
        "absolute_threshold_lufs": report_json_number(
          Some(gate.absolute_threshold_lufs()),
        ),
        "relative_threshold_lufs": report_json_number(
          Some(gate.relative_threshold_lufs()),
        ),
        "applied_threshold_lufs": report_json_number(
          Some(gate.applied_threshold_lufs()),
        ),
        "candidate_block_count": Json(gate.candidate_block_count()),
        "gated_block_count": Json(gate.gated_block_count()),
        "integrated_lufs": report_json_number(Some(gate.integrated_lufs())),
      })
  }
}

///|
fn report_lra_gate_json(gate : LraGateReport?) -> Json {
  match gate {
    None => Json::null()
    Some(gate) =>
      Json::object({
        "absolute_threshold_lufs": report_json_number(
          Some(gate.absolute_threshold_lufs()),
        ),
        "relative_threshold_lufs": report_json_number(
          Some(gate.relative_threshold_lufs()),
        ),
        "applied_threshold_lufs": report_json_number(
          Some(gate.applied_threshold_lufs()),
        ),
        "candidate_block_count": Json(gate.candidate_block_count()),
        "gated_block_count": Json(gate.gated_block_count()),
        "percentile_10_lufs": report_json_number(
          Some(gate.percentile_10_lufs()),
        ),
        "percentile_95_lufs": report_json_number(
          Some(gate.percentile_95_lufs()),
        ),
        "loudness_range_lu": report_json_number(Some(gate.loudness_range_lu())),
      })
  }
}

///|
/// Detached machine-readable report, schema "moonloudness.measurement" v1.
/// Keys are inserted in a fixed order, with no timestamps or mutable state.
/// All optional measurements and gates are present: unavailable/non-finite
/// numeric values become null, never NaN/Infinity. Counters are JSON numbers;
/// units are explicit in field names (Hz, seconds, LUFS, LU, dBFS, dBTP).
/// Layout is inferred from the public channel-peak count for the supported
/// mono/stereo/5.0 layouts. Unknown layouts have null names/labels, not a guess.
/// Nested loudness, gates, and peaks preserve the public getters verbatim.
/// Every object/array is newly allocated; editing returned JSON cannot change
/// the report, analyzer, cached report, or later exports.
pub fn MeasurementReport::to_json(self : MeasurementReport) -> Json {
  let channel_peaks = self.channel_sample_peaks_linear()
  let layout : ChannelLayout? = match channel_peaks.length() {
    1 => Some(ChannelLayout::mono())
    2 => Some(ChannelLayout::stereo())
    5 => Some(ChannelLayout::five_point_zero())
    _ => None
  }
  let name : Json = match channel_peaks.length() {
    1 => Json::string("mono")
    2 => Json::string("stereo")
    5 => Json::string("5.0")
    _ => Json::null()
  }
  let labels : Array[Json] = []
  let peaks : Array[Json] = []
  for index in 0..
        match layout.channel_label(index) {
          Some(label) => Json::string(label)
          None => Json::null()
        }
      None => Json::null()
    }
    labels.push(label)
    let peak = channel_peaks[index]
    // Construct separate values so even the two returned label containers own
    // their structure; labels themselves are immutable JSON strings/nulls.
    peaks.push(
      Json::object({
        "label": label,
        "sample_linear": report_json_number(Some(peak)),
        "sample_dbfs": report_json_number(amplitude_to_db(peak)),
      }),
    )
  }
  Json::object({
    "schema": Json::string("moonloudness.measurement"),
    "schema_version": Json(1),
    "sample_rate_hz": Json(self.sample_rate()),
    "frame_count": Json(self.frame_count()),
    "duration_seconds": report_json_number(Some(self.duration_seconds())),
    "layout": Json::object({
      "name": name,
      "channel_count": Json(channel_peaks.length()),
      "channel_labels": Json::array(labels),
    }),
    "loudness": Json::object({
      "integrated_lufs": report_json_number(self.integrated_lufs()),
      "loudness_range_lu": report_json_number(self.loudness_range_lu()),
      "momentary_lufs": report_json_number(self.momentary_lufs()),
      "short_term_lufs": report_json_number(self.short_term_lufs()),
    }),
    "availability": Json::object({
      "integrated": self.integrated_status().as_json(),
      "lra": self.lra_status().as_json(),
    }),
    "gates": Json::object({
      "integrated": report_integrated_gate_json(self.integrated_gate()),
      "lra": report_lra_gate_json(self.lra_gate()),
    }),
    "peaks": Json::object({
      "sample_linear": report_json_number(Some(self.sample_peak_linear())),
      "sample_dbfs": report_json_number(self.sample_peak_dbfs()),
      "true_linear": report_json_number(Some(self.true_peak_linear())),
      "true_dbtp": report_json_number(self.true_peak_dbtp()),
      "channels": Json::array(peaks),
    }),
  })
}

///|
test "report JSON guards non-finite numeric values" {
  assert_eq(report_json_number(Some(0.0 / 0.0)), Json::null())
  assert_eq(report_json_number(Some(1.0 / 0.0)), Json::null())
  assert_eq(report_json_number(Some(-1.0 / 0.0)), Json::null())
  assert_eq(report_json_number(None), Json::null())
  assert_eq(report_json_number(Some(0.0)), Json::number(0.0))
}