///|
/// Common-grid features; conditions flag compares only complex kind and cutoff.
/// Units, normalization and sampling comparability remain properties of inputs.
pub fn batch_features(
  analyses : Array[Analysis],
  names : Array[String],
  dimension~ : Int,
  start~ : Double,
  end~ : Double,
  samples? : Int = 101,
  layers? : Int = 3,
) -> (Json, String) raise TopologyError {
  if analyses.length() < 1 ||
    analyses.length() > 8 ||
    names.length() != analyses.length() {
    raise TopologyError("features require 1..8 named analyses")
  }
  if dimension < 0 ||
    dimension > 2 ||
    layers < 1 ||
    layers > 16 ||
    !finite(start) ||
    !finite(end) ||
    start.abs() > 1.0e100 ||
    end.abs() > 1.0e100 ||
    start >= end {
    raise TopologyError("invalid common feature dimension, layers or range")
  }
  let seen : Array[String] = []
  for name in names {
    batch_name(name)
    if seen.contains(name) {
      raise TopologyError("duplicate feature name")
    }
    seen.push(name)
  }
  let grid = if samples >= 2 && samples <= 1001 {
    landscape_grid(start, end, samples)
  } else {
    raise TopologyError("feature samples must be 2..1001")
  }
  let columns : Array[String] = [
    "case", "kind", "cutoff", "dimension", "censored_intervals_excluded", "finite_persistence_entropy",
    "betti_at_end",
  ]
  for i = 0; i < samples; i = i + 1 {
    columns.push("betti_\{i}")
  }
  for layer = 0; layer < layers; layer = layer + 1 {
    for i = 0; i < samples; i = i + 1 {
      columns.push("lambda_\{layer + 1}_\{i}")
    }
  }
  let mut csv = columns.join(",") + "\n"
  let rows : Array[Json] = []
  let mut same = true
  for i, analysis in analyses {
    if !finite(start) ||
      !finite(end) ||
      start >= end ||
      end > analysis.filtration.cutoff {
      raise TopologyError(
        "common feature range requires finite start < end <= every cutoff",
      )
    }
    if analysis.filtration.kind != analyses[0].filtration.kind ||
      analysis.filtration.cutoff != analyses[0].filtration.cutoff {
      same = false
    }
    let landscape = persistence_landscape(
      analysis,
      dimension~,
      start~,
      end~,
      samples~,
      layers~,
    )
    let vector : Array[Double] = []
    for layer in landscape {
      for value in layer {
        vector.push(value)
      }
    }
    let curve : Array[Int] = []
    for scale in grid {
      curve.push(betti(analysis, dimension, scale))
    }
    let finite_intervals = analysis.intervals.filter(fn(interval) {
      interval.dimension == dimension && interval.death is Some(_)
    })
    let excluded = analysis.intervals
      .filter(fn(interval) {
        interval.dimension == dimension && interval.death is None
      })
      .length()
    let mut total = 0.0
    for interval in finite_intervals {
      total += observed_lifetime(analysis, interval)
    }
    let mut entropy = 0.0
    if total > 0.0 {
      for interval in finite_intervals {
        let p = observed_lifetime(analysis, interval) / total
        if p > 0.0 {
          entropy -= p * @math.ln(p)
        }
      }
    }
    csv += "\{names[i]},\{analysis.filtration.kind},\{analysis.filtration.cutoff},\{dimension},\{excluded},\{entropy},\{betti(analysis, dimension, end)}"
    for value in curve {
      csv += ",\{value}"
    }
    for value in vector {
      csv += ",\{value}"
    }
    csv += "\n"
    rows.push({
      "name": names[i],
      "kind": analysis.filtration.kind,
      "cutoff": analysis.filtration.cutoff,
      "dimension": dimension,
      "censored_intervals_excluded": excluded,
      "finite_persistence_entropy": entropy,
      "betti_at_end": betti(analysis, dimension, end),
      "betti_vector": curve,
      "landscape_vector": vector,
    })
  }
  (
    {
      "schema_version": 1,
      "dimension": dimension,
      "start": start,
      "end": end,
      "samples": samples,
      "layers": layers,
      "grid": grid,
      "columns": columns,
      "rows": rows,
      "complex_kinds_and_cutoffs_equal": same,
      "landscape_order": "layer-major, increasing scale",
      "entropy_scope": "all finite intervals through each input cutoff",
      "note": "Betti includes censored intervals; landscape and entropy exclude them. Equal kind and cutoff do not establish equal units, preprocessing or sampling.",
    },
    csv,
  )
}