///|
/// Reference-range helpers for population and personal comparisons.

///|
/// A reference range with a label and sample provenance.
pub(all) struct ReferenceRange {
  name : String
  lower : Double
  upper : Double
  center : Double
  sample_count : Int
  source : String
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Position of a value within a reference range.
pub(all) struct RangePosition {
  value : Double
  percentile : Double
  z_score : Double
  band : String
  in_range : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Build a robust reference range from observed values.
pub fn make_reference_range(
  values : Array[Double],
  name : String,
  source : String,
) -> ReferenceRange {
  let distribution = summarize_distribution(values)
  {
    name,
    lower: distribution.q1 - 1.5 * (distribution.q3 - distribution.q1),
    upper: distribution.q3 + 1.5 * (distribution.q3 - distribution.q1),
    center: distribution.median,
    sample_count: values.length(),
    source,
  }
}

///|
/// Calculate a range position using a supplied standard deviation.
pub fn position_in_reference_range(
  value : Double,
  range : ReferenceRange,
  standard_deviation_value : Double,
  population_values : Array[Double],
) -> RangePosition {
  let scale = if standard_deviation_value <= 0.0 {
    1.0
  } else {
    standard_deviation_value
  }
  let z = (value - range.center) / scale
  let percentile = percentile_rank(population_values, value)
  let band = if value < range.lower {
    "below"
  } else if value > range.upper {
    "above"
  } else if value < range.center {
    "lower_half"
  } else {
    "upper_half"
  }
  {
    value,
    percentile,
    z_score: z,
    band,
    in_range: value >= range.lower && value <= range.upper,
  }
}

///|
/// Return a normalized distance from the center of a range.
pub fn range_center_distance(value : Double, range : ReferenceRange) -> Double {
  let width = range.upper - range.lower
  if width <= 0.0 {
    0.0
  } else {
    (value - range.center) / width
  }
}

///|
/// Clamp a value into a reference range.
pub fn clamp_to_reference_range(
  value : Double,
  range : ReferenceRange,
) -> Double {
  if value < range.lower {
    range.lower
  } else if value > range.upper {
    range.upper
  } else {
    value
  }
}

///|
/// Calculate a robust personal reference range from a history.
pub fn personal_rmssd_range(
  history : Array[MorningMeasurement],
) -> ReferenceRange {
  let values = []
  for measurement in history {
    if measurement.rmssd > 0.0 {
      values.push(measurement.rmssd)
    }
  }
  make_reference_range(values, "personal_rmssd", "morning_history")
}

///|
/// Count observations inside a reference range.
pub fn count_in_reference_range(
  values : Array[Double],
  range : ReferenceRange,
) -> Int {
  let mut count = 0
  for value in values {
    if value >= range.lower && value <= range.upper {
      count += 1
    }
  }
  count
}

///|
/// Calculate the coverage ratio of a range.
pub fn reference_range_coverage(
  values : Array[Double],
  range : ReferenceRange,
) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    count_in_reference_range(values, range).to_double() /
    values.length().to_double()
  }
}

///|
/// Return the widest of several reference ranges.
pub fn merge_reference_ranges(
  ranges : Array[ReferenceRange],
) -> ReferenceRange? {
  if ranges.length() == 0 {
    return None
  }
  let mut lower = ranges[0].lower
  let mut upper = ranges[0].upper
  let mut center = ranges[0].center
  let mut sample_count = 0
  let mut source = ranges[0].source
  for range in ranges {
    if range.lower < lower {
      lower = range.lower
    }
    if range.upper > upper {
      upper = range.upper
    }
    center += range.center
    sample_count += range.sample_count
    source = source + "+" + range.source
  }
  Some({
    name: ranges[0].name,
    lower,
    upper,
    center: center / ranges.length().to_double(),
    sample_count,
    source,
  })
}

///|
/// Create a range feature vector.
pub fn reference_range_feature_vector(
  position : RangePosition,
) -> Array[Double] {
  [
    position.value,
    position.percentile,
    position.z_score,
    if position.in_range {
      1.0
    } else {
      0.0
    },
  ]
}

///|
/// Return a plain-language range interpretation.
pub fn reference_range_interpretation(position : RangePosition) -> String {
  if position.band == "below" {
    "value is below the reference range"
  } else if position.band == "above" {
    "value is above the reference range"
  } else {
    "value is inside the reference range"
  }
}