///|
/// Observed lifetime; for a censored class this is only a lower bound.
pub fn observed_lifetime(analysis : Analysis, interval : Interval) -> Double {
  let end = match interval.death {
    Some(value) => value
    None => analysis.filtration.cutoff
  }
  maximum(0.0, end - interval.birth)
}

///|
/// Select and rank intervals by observed lifetime without modifying the result.
/// Undead intervals use cutoff-birth; ranking is not proof of infinite lifetime.
pub fn ranked_intervals(
  analysis : Analysis,
  dimension~ : Int,
  min_lifetime? : Double = 0.0,
  include_alive? : Bool = true,
) -> Array[Interval] raise TopologyError {
  if dimension < 0 ||
    dimension > 1 ||
    !finite(min_lifetime) ||
    min_lifetime < 0.0 {
    raise TopologyError(
      "interval selection requires dimension 0/1 and nonnegative finite minimum lifetime",
    )
  }
  let result = analysis.intervals.filter(fn(interval) {
    interval.dimension == dimension &&
    (include_alive || interval.death is Some(_)) &&
    observed_lifetime(analysis, interval) >= min_lifetime
  })
  result.sort_by(fn(a, b) {
    let da = observed_lifetime(analysis, a)
    let db = observed_lifetime(analysis, b)
    if da > db {
      -1
    } else if da < db {
      1
    } else {
      a.birth_cell - b.birth_cell
    }
  })
  result
}

///|
/// Per-dimension counts, finite lifetimes, finite persistence entropy and
/// Betti numbers at cutoff. Censored intervals never enter finite entropy.
pub fn summary_json(analysis : Analysis) -> Json {
  let summaries : Array[Json] = []
  for dimension = 0; dimension < 2; dimension = dimension + 1 {
    let mut finite_count = 0
    let mut alive_count = 0
    let mut finite_total = 0.0
    let mut observed_total = 0.0
    let mut longest = 0.0
    for interval in analysis.intervals {
      if interval.dimension != dimension {
        continue
      }
      let life = observed_lifetime(analysis, interval)
      observed_total += life
      match interval.death {
        None => alive_count += 1
        Some(_) => {
          finite_count += 1
          finite_total += life
          longest = maximum(longest, life)
        }
      }
    }
    let mut entropy = 0.0
    if finite_total > 0.0 {
      for interval in analysis.intervals {
        if interval.dimension != dimension || interval.death is None {
          continue
        }
        let p = observed_lifetime(analysis, interval) / finite_total
        if p > 0.0 {
          entropy -= p * @math.ln(p)
        }
      }
    }
    let longest_finite : Json = if finite_count == 0 {
      Json::null()
    } else {
      longest.to_json()
    }
    summaries.push({
      "dimension": dimension,
      "finite_count": finite_count,
      "alive_at_cutoff": alive_count,
      "total_finite_lifetime": finite_total,
      "total_observed_lifetime": observed_total,
      "longest_finite_lifetime": longest_finite,
      "finite_persistence_entropy": entropy,
    })
  }
  Json::array(summaries)
}

///|
/// Numeric CSV with complete intervals and a semicolon-separated chain column.
pub fn intervals_csv(analysis : Analysis) -> String {
  let mut text = "index,dimension,birth,death,alive_at_cutoff,observed_lifetime,representative_cells\n"
  for i = 0; i < analysis.intervals.length(); i = i + 1 {
    let interval = analysis.intervals[i]
    let death = match interval.death {
      None => ""
      Some(d) => d.to_string()
    }
    let mut chain = ""
    for j = 0; j < interval.representative.length(); j = j + 1 {
      if j > 0 {
        chain += ";"
      }
      chain += interval.representative[j].to_string()
    }
    text += "\{i},\{interval.dimension},\{interval.birth},\{death},\{interval.death is None},\{observed_lifetime(analysis, interval)},\{chain}\n"
  }
  text
}

///|
/// Uniform samples over the filtration's observed range, including cutoff.
pub fn betti_csv(
  analysis : Analysis,
  samples? : Int = 101,
) -> String raise TopologyError {
  if samples < 2 || samples > 1001 {
    raise TopologyError("Betti CSV requires 2..1001 samples")
  }
  let mut start = analysis.filtration.cutoff
  for cell in analysis.filtration.cells {
    if cell.value < start {
      start = cell.value
    }
  }
  let mut text = "scale,H0,H1\n"
  for i = 0; i < samples; i = i + 1 {
    let scale = if i == samples - 1 {
      analysis.filtration.cutoff
    } else {
      start +
      (analysis.filtration.cutoff - start) *
      i.to_double() /
      (samples - 1).to_double()
    }
    text += "\{scale},\{betti(analysis, 0, scale)},\{betti(analysis, 1, scale)}\n"
  }
  text
}